Skip to content

Commit

Permalink
feat: daterange picker
Browse files Browse the repository at this point in the history
  • Loading branch information
ObservedObserver committed Dec 12, 2023
1 parent 360b496 commit 0e4154a
Show file tree
Hide file tree
Showing 7 changed files with 116 additions and 57 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Check docs and compoenent examples in [![Streamlit App](https://static.streamlit
+ [x] card
+ [x] avatar
+ [x] date_picker
+ [ ] date_range_picker
+ [x] date_range_picker (date_picker with mode="range")
+ [x] table
+ [x] input
+ [x] slider
Expand Down
8 changes: 5 additions & 3 deletions pages/DatePicker.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@
with open("docs/components/date_picker.md", "r") as f:
st.markdown(f.read())

dt = ui.date_picker(key="date_picker", label="Date Picker")
dt = ui.date_picker(key="date_picker", mode="single", label="Date Picker")

st.write("Date:", dt)
st.write("Date Value:", dt)

dt2 = ui.date_picker(key="date_picker2", label="Date Picker")
dt2 = ui.date_picker(key="date_picker2", mode="range", label="Date Picker")

st.write("Date Range:", dt2)


st.write(ui.date_picker)
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "streamlit-shadcn-ui"
version = "0.1.17"
version = "0.1.18"
readme = "README.md"
keywords = ["streamlit", "shadcn", "ui", "components"]
description = "Using shadcn components in Streamlit"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,43 +6,96 @@ import {
PopoverTrigger,
} from "@/components/ui/popover";
import { useBodyStyle } from "@/hooks/useBodyStyle";
import { format, parse } from "date-fns";
import { forwardRef, useEffect, useState } from "react";
import { Streamlit } from "streamlit-component-lib";

function formatDate(date: Date): string {
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, '0'); // Months are 0-indexed
const day = date.getDate().toString().padStart(2, '0');
type DateRange = {
from: Date;
to?: Date;
};

return `${year}-${month}-${day}`;
function formatDate(date: Date | DateRange | string | string[]): string | [string, string] {
const formatSingleDate = (date: Date) => {
return format(date, "yyyy-MM-dd");
}
if (date instanceof Date) {
return formatSingleDate(date);
} else if (typeof date === "string") {
const d = parse(date, 'yyyy-MM-dd', new Date());
return formatSingleDate(d);
} else if (Array.isArray(date)) {
return [formatDate(date[0]), formatDate(date[1])] as [string, string];
} else {
return [formatDate(date.from), formatDate(date.to)] as [string, string];
}
}

interface StDatePickerContentProps {
value?: string;
type StDatePickerContentProps =
| {
value?: string;
mode: "single";
}
| {
value?: [string, string];
mode: "range";
};

function initDateValue(props: StDatePickerContentProps): Date | DateRange {
const { value, mode } = props;
if (mode === "single") {
return parse(value, 'yyyy-MM-dd', new Date());
} else {
if (!value) {
return {
from: new Date(),
};
}
return {
from: parse(value[0], 'yyyy-MM-dd', new Date()),
to: parse(value[1], 'yyyy-MM-dd', new Date()),
};
}
}

export const StDatePickerContent = forwardRef<
HTMLDivElement,
StDatePickerContentProps
>((props, ref) => {
const { value } = props;
const [date, setDate] = useState<Date>(new Date(value));
const { value, mode } = props;
const [date, setDate] = useState<Date | DateRange>(initDateValue(props));

useEffect(() => {
setDate(new Date(value));
}, [value]);
setDate(
initDateValue({
value,
mode,
} as StDatePickerContentProps)
);
}, [value, mode]);

useBodyStyle("body { background-color: transparent !important; }")
useBodyStyle("body, document { background-color: transparent !important; }");

return (
<Popover open={true}>
<PopoverTrigger className="hidden"></PopoverTrigger>
<PopoverContent ref={ref} className="w-auto p-0">
<Calendar
mode="single"
selected={date}
onSelect={setDate}
initialFocus
/>
{
mode === 'single' && <Calendar
mode="single"
selected={date as Date}
onSelect={setDate as (date: Date) => void}
initialFocus
/>
}
{
mode === 'range' && <Calendar
mode="range"
selected={date as DateRange}
onSelect={setDate as (date: DateRange) => void}
initialFocus
/>
}
<div className="mb-4 mx-4 flex gap-2">
<Button
onClick={() => {
Expand All @@ -58,7 +111,9 @@ export const StDatePickerContent = forwardRef<
variant="secondary"
onClick={() => {
Streamlit.setComponentValue({
value: value ? formatDate(new Date(value)) : value,
value: value
? formatDate(value)
: value,
open: false,
});
}}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import { format } from "date-fns";
import { format, parse } from "date-fns";
import { Calendar as CalendarIcon } from "lucide-react";

import { cn, getPositionRelativeToTopDocument } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Popover, PopoverTrigger } from "@/components/ui/popover";
import { forwardRef, useEffect, useState } from "react";
import { Streamlit } from "streamlit-component-lib";
import { useBodyStyle } from "@/hooks/useBodyStyle";
import { Label } from "@/components/ui/label";

interface StDatePickerTriggerProps {
value?: string;
value?: string | string[];
open: boolean;
label?: string;
}
Expand All @@ -19,17 +18,21 @@ export const StDatePickerTrigger = forwardRef<
HTMLDivElement,
StDatePickerTriggerProps
>((props, ref) => {
const { label } = props;
const { label, value } = props;
const [open, setOpen] = useState(Boolean(props.open));

const date = props.value ? new Date(props.value) : null;
const date: Date[] = value
? value instanceof Array
? value.map((v) => parse(v, 'yyyy-MM-dd', new Date()))
: [parse(value, 'yyyy-MM-dd', new Date())]
: null;

useEffect(() => {
setOpen(Boolean(props.open));
}, [props.open]);

useEffect(() => {
if (ref && typeof ref !== 'function') {
if (ref && typeof ref !== "function") {
const pos = getPositionRelativeToTopDocument(ref.current);

Streamlit.setComponentValue({
Expand All @@ -46,25 +49,25 @@ export const StDatePickerTrigger = forwardRef<
useBodyStyle("body { padding-right: 0.5em !important; }");

return (
<Popover>
<PopoverTrigger asChild>
<div className="m-1" ref={ref}>
{label && <Label className="mb-2 block">{label}</Label>}
<Button
variant={"outline"}
className={cn(
"w-[280px] justify-start text-left font-normal",
!date && "text-muted-foreground"
)}
onClick={() => {
setOpen((v) => !v);
}}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{date ? format(date, "PPP") : <span>Pick a date</span>}
</Button>
</div>
</PopoverTrigger>
</Popover>
<div className="m-1" ref={ref}>
{label && <Label className="mb-2 block">{label}</Label>}
<Button
variant={"outline"}
className={cn(
"w-[280px] justify-start text-left font-normal",
!date && "text-muted-foreground"
)}
onClick={() => {
setOpen((v) => !v);
}}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{date ? (
date.map((d) => format(d, "yyyy-MM-dd")).join(" - ")
) : (
<span>Pick a date</span>
)}
</Button>
</div>
);
});
1 change: 0 additions & 1 deletion streamlit_shadcn_ui/py_components/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@

# variant "default" | "destructive" | "outline" | "secondary" | "ghost" | "link"


def button(text: str, variant: str = "default", class_name: str = None, key=None, **kwargs):
props = {"text": text, "variant": variant, "className": class_name, **kwargs}
default_state = init_default_state(key, default_value=False)
Expand Down
12 changes: 6 additions & 6 deletions streamlit_shadcn_ui/py_components/date_picker.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ def date_picker_trigger(value = None, label: str = None, open_status = False, ke
props = {"value": value, "open": open_status, "label": label}
return _component_func(comp=name, props=props, key=key, default={"x": 0, "y": 0, "open": False})

def date_picker_content(x: int, y: int, value=None, default_value=None, open: bool = False, key=None, on_change=None, args=None, kwargs=None):
def date_picker_content(x: int, y: int, mode: str, value=None, default_value=None, open: bool = False, key=None, on_change=None, args=None, kwargs=None):
name = "date_picker_content"
_component_func = declare_component(name)
register_callback(key=key, callback=on_change, args=args, kwargs=kwargs)
Expand All @@ -21,26 +21,26 @@ def date_picker_content(x: int, y: int, value=None, default_value=None, open: bo
top: {y}px;
left: {x}px;
display: { "block" if open else "none" };
background-color: transparent;
z-index: 1000;
}}
""")
with container:
props = {"value": value}
props = {"value": value, "mode": mode}
result = _component_func(comp=name, props=props, key=key, default={"value": default_value, "open": False})

return result

def date_choosen_handler(from_key, to_key):
st.session_state[to_key]['open'] = st.session_state[from_key]['open']

def date_picker(label=None, default_value=None, key=None):
def date_picker(label=None, mode="single", default_value=None, key=None):
trigger_component_key = f"trigger_{key}"
content_component_key = f"content_{key}"
init_session(key=trigger_component_key, default_value={"x": 0, "y": 0, "open": False})
init_session(key=content_component_key, default_value={"value": default_value, "open": False})
open_status = st.session_state[trigger_component_key]['open']
with stylable_container(key=f"root_{key}", css_styles="""
with stylable_container(key=f"root_{key}", css_styles="""
{
position: relative;
}
Expand All @@ -49,6 +49,6 @@ def date_picker(label=None, default_value=None, key=None):
trigger_pos = date_picker_trigger(value=value, label=label, open_status=open_status, key=trigger_component_key)
# need to sync value, or "cancel" will not work
st.session_state[content_component_key]['open'] = trigger_pos['open']
content_state = date_picker_content(value=value, x=trigger_pos['x'], y=trigger_pos['y'], open=open_status, key=content_component_key, on_change=date_choosen_handler, kwargs={"from_key": content_component_key, "to_key": trigger_component_key})
content_state = date_picker_content(value=value, mode=mode, x=trigger_pos['x'], y=trigger_pos['y'], open=open_status, key=content_component_key, on_change=date_choosen_handler, kwargs={"from_key": content_component_key, "to_key": trigger_component_key})
value = content_state['value']
return value

0 comments on commit 0e4154a

Please sign in to comment.