diff --git a/pynecone/components/__init__.py b/pynecone/components/__init__.py index 8dd3f07b0..0c5998cb1 100644 --- a/pynecone/components/__init__.py +++ b/pynecone/components/__init__.py @@ -105,6 +105,8 @@ input = Input.create input_group = InputGroup.create input_left_addon = InputLeftAddon.create input_right_addon = InputRightAddon.create +multi_select = MultiSelect +multi_select_option = MultiSelectOption number_decrement_stepper = NumberDecrementStepper.create number_increment_stepper = NumberIncrementStepper.create number_input = NumberInput.create diff --git a/pynecone/components/forms/__init__.py b/pynecone/components/forms/__init__.py index 936b481be..aeb38fb7b 100644 --- a/pynecone/components/forms/__init__.py +++ b/pynecone/components/forms/__init__.py @@ -8,6 +8,8 @@ from .email import Email from .form import Form, FormControl, FormErrorMessage, FormHelperText, FormLabel from .iconbutton import IconButton from .input import Input, InputGroup, InputLeftAddon, InputRightAddon +from .multiselect import Option as MultiSelectOption +from .multiselect import Select as MultiSelect from .numberinput import ( NumberDecrementStepper, NumberIncrementStepper, diff --git a/pynecone/components/forms/multiselect.py b/pynecone/components/forms/multiselect.py new file mode 100644 index 000000000..328c4f1fe --- /dev/null +++ b/pynecone/components/forms/multiselect.py @@ -0,0 +1,346 @@ +"""Provides a feature-rich Select and some (not all) related components.""" + +from typing import Any, Dict, List, Optional, Set, Union + +from pynecone.base import Base +from pynecone.components.component import Component +from pynecone.event import EVENT_ARG +from pynecone.vars import Var + + +class Option(Base): + """An option component for the chakra-react-select Select.""" + + # What is displayed to the user + label: str + + # The value of the option, must be serializable + value: Any + + # the variant of the option tag + variant: Optional[str] = None + + # [not working yet] + # Whether the option is disabled + # is_disabled: Optional[bool] = None + + # [not working yet] + # The visual color appearance of the component + # options: "whiteAlpha" | "blackAlpha" | "gray" | "red" | + # "orange" | "yellow" | "green" | "teal" | "blue" | "cyan" | + # "purple" | "pink" | "linkedin" | "facebook" | "messenger" | + # "whatsapp" | "twitter" | "telegram" + # default: "gray" + # color_scheme: Optional[str] = None + + # [not working yet] + # The icon of the option tag + # icon: Optional[str] = None + + +class Select(Component): + """The default chakra-react-select Select component. + Not every available prop is listed here, + for a complete overview check the react-select/chakra-react-select docs. + Props added by chakra-react-select are marked with "[chakra]". + """ + + library = "chakra-react-select" + tag = "Select" + + # Focus the control when it is mounted + auto_focus: Var[bool] + + # Remove the currently focused option when the user presses backspace + # when Select isClearable or isMulti + backspace_removes_value: Var[bool] + + # Remove focus from the input when the user selects an option + # (handy for dismissing the keyboard on touch devices) + blur_input_on_select: Var[bool] + + # When the user reaches the top/bottom of the menu, + # prevent scroll on the scroll-parent + capture_menu_scroll: Var[bool] + + # [chakra] + # To use the chakraStyles prop, first, + # check the documentation for the original styles prop from the react-select docs. + # This package offers an identical API for the chakraStyles prop, however, + # the provided and output style objects use Chakra's sx prop + # instead of the default emotion styles the original package offers. + # This allows you to both use the shorthand styling props you'd normally use + # to style Chakra components, as well as tokens from your theme such as named colors. + # All of the style keys offered in the original package can be used in the chakraStyles prop + # except for menuPortal. Along with some other caveats, this is explained below. + # Most of the components rendered by this package use the basic Chakra component with a few exceptions. + # Here are the style keys offered and the corresponding Chakra component that is rendered: + # - clearIndicator - Box (uses theme styles for Chakra's CloseButton) + # - container - Box + # - control - Box (uses theme styles for Chakra's Input) + # - dropdownIndicator - Box (uses theme styles for Chrakra's InputRightAddon) + # - downChevron - Icon + # - crossIcon - Icon + # - group - Box + # - groupHeading - Box (uses theme styles for Chakra's Menu group title) + # - indicatorsContainer - Box + # - indicatorSeparator - Divider + # - input - chakra.input (wrapped in a Box) + # - inputContainer - Box + # - loadingIndicator - Spinner + # - loadingMessage - Box + # - menu - Box + # - menuList - Box (uses theme styles for Chakra's Menu) + # - multiValue - chakra.span (uses theme styles for Chakra's Tag) + # - multiValueLabel - chakra.span (uses theme styles for Chakra's TagLabel) + # - multiValueRemove - Box (uses theme styles for Chakra's TagCloseButton) + # - noOptionsMessage - Box + # - option - Box (uses theme styles for Chakra's MenuItem) + # - placeholder - Box + # - singleValue - Box + # - valueContainer - Box + chakra_styles: Var[str] + + # Close the select menu when the user selects an option + close_menu_on_select: Var[bool] + + # If true, close the select menu when the user scrolls the document/body. + close_menu_on_scroll: Var[bool] + + # [chakra] + # The visual color appearance of the component + # options: "whiteAlpha" | "blackAlpha" | "gray" | "red" | + # "orange" | "yellow" | "green" | "teal" | "blue" | "cyan" | + # "purple" | "pink" | "linkedin" | "facebook" | "messenger" | + # "whatsapp" | "twitter" | "telegram" + # default: "gray" + color_scheme: Var[str] + + # This complex object includes all the compositional components + # that are used in react-select. If you wish to overwrite a component, + # pass in an object with the appropriate namespace. + # If you only wish to restyle a component, + # we recommend using the styles prop instead. + components: Var[Dict[str, Component]] + + # Whether the value of the select, e.g. SingleValue, + # should be displayed in the control. + control_should_render_value: Var[bool] + + # Delimiter used to join multiple values into a single HTML Input value + delimiter: Var[str] + + # [chakra] + # Colors the component border with the given chakra color string on error state + # default: "red.500" + error_border_color: Var[str] + + # Clear all values when the user presses escape AND the menu is closed + escape_clears_value: Var[bool] + + # [chakra] + # Colors the component border with the given chakra color string on focus + # default: "blue.500" + focus_border_color: Var[str] + + # Sets the form attribute on the input + form: Var[str] + + # Hide the selected option from the menu + hide_selected_options: Var[bool] + + # The id to set on the SelectContainer component. + # id: Var[str] + + # The value of the search input + input_value: Var[str] + + # The id of the search input + input_id: Var[str] + + # Is the select value clearable + is_clearable: Var[bool] + + # Is the select disabled + is_disabled: Var[bool] + + # [chakra] + # Style component in the chakra invalid style + # default: False + is_invalid: Var[bool] + + # Support multiple selected options + is_multi: Var[bool] + + # [chakra] + # Style component as disabled (chakra style) + # default: False + is_read_only: Var[bool] + + # Is the select direction right-to-left + is_rtl: Var[bool] + + # Whether to enable search functionality + is_searchable: Var[bool] + + # Minimum height of the menu before flipping + min_menu_height: Var[int] + + # Maximum height of the menu before scrolling + max_menu_height: Var[int] + + # Default placement of the menu in relation to the control. + # 'auto' will flip when there isn't enough space below the control. + # options: "bottom" | "auto" | "top" + menu_placement: Var[str] + + # The CSS position value of the menu, + # when "fixed" extra layout management is required + # options: "absolute" | "fixed" + menu_position: Var[str] + + # Whether to block scroll events when the menu is open + menu_should_block_scroll: Var[bool] + + # Whether the menu should be scrolled into view when it opens + menu_should_scroll_into_view: Var[bool] + + # Name of the HTML Input (optional - without this, no input will be rendered) + name: Var[str] + + # Allows control of whether the menu is opened when the Select is focused + open_menu_on_focus: Var[bool] + + # Allows control of whether the menu is opened when the Select is clicked + open_menu_on_click: Var[bool] + + # Array of options that populate the select menu + options: Var[List[Dict]] + + # Number of options to jump in menu when page{up|down} keys are used + page_size: Var[int] + + # Placeholder for the select value + placeholder: Var[Optional[str]] + + # Marks the value-holding input as required for form validation + required: Var[bool] + + # [chakra] + # If you choose to stick with the default selectedOptionStyle="color", + # you have one additional styling option. + # If you do not like the default of blue for the highlight color, + # you can pass the selectedOptionColorScheme prop to change it. + # This prop will accept any named color from your theme's color palette, + # and it will use the 500 value in light mode or the 300 value in dark mode. + # This prop can only be used for named colors from your theme, not arbitrary hex/rgb colors. + # If you would like to use a specific color for the background that's not a part of your theme, + # use the chakraStyles prop to customize it. + # default: "blue" + selected_option_color_scheme: Var[str] + + # [chakra] + # The default option "color" will style a selected option + # similar to how react-select does it, + # by highlighting the selected option in the color blue. + # Alternatively, if you pass "check" for the value, + # the selected option will be styled like the Chakra UI Menu component + # and include a check icon next to the selected option(s). + # If is_multi and selected_option_style="check" are passed, + # space will only be added for the check marks + # if hide_selected_options=False is also passed. + # options: "color" | "check" + # default: "color" + selected_option_style: Var[str] + + # [chakra] + # The size of the component. + # options: "sm" | "md" | "lg" + # default: "md" + size: Var[str] + + # Sets the tabIndex attribute on the input + tab_index: Var[int] + + # Select the currently focused option when the user presses tab + tab_selects_value: Var[bool] + + # [chakra] + # Variant of multi-select tags + # options: "subtle" | "solid" | "outline" + # default: "subtle" + tag_variant: Var[str] + + # Remove all non-essential styles + unstyled: Var[bool] + + # [chakra] + # If this prop is passed, + # the dropdown indicator at the right of the component will be styled + # in the same way the original Chakra Select component is styled, + # instead of being styled as an InputRightAddon. + # The original purpose of styling it as an addon + # was to create a visual separation between the dropdown indicator + # and the button for clearing the selected options. + # However, as this button only appears when isMulti is passed, + # using this style could make more sense for a single select. + # default: False + use_basic_style: Var[bool] + + # [chakra] + # The variant of the Select. If no variant is passed, + # it will default to defaultProps.variant from the theme for Chakra's Input component. + # If your component theme for Input is not modified, it will be outline. + # options: "outline" | "filled" | "flushed" | "unstyled" + # default: "outline" + variant: Var[str] + + # How the options should be displayed in the menu. + menu_position: Var[str] = "fixed" # type: ignore + + def get_controlled_triggers(self) -> Dict[str, Var]: + """Get the event triggers that pass the component's value to the handler. + + Returns: + A dict mapping the event trigger to the var that is passed to the handler. + """ + # A normal select returns the value. + value = EVENT_ARG.value + + # Multi-select returns a list of values. + if self.is_multi: + value = Var.create_safe(f"{EVENT_ARG}.map(e => e.value)", is_local=True) + return {"on_change": value} + + @classmethod + def get_initial_props(cls) -> Set[str]: + """Get the initial props to set for the component. + + Returns: + The initial props to set. + """ + return super().get_initial_props() | {"is_multi"} + + @classmethod + def create( + cls, options: List[Union[Option, str, int, float, bool]], **props + ) -> Component: + """Takes a list of options and additional properties, checks if each option is an + instance of Option, and returns a Select component with the given options and + properties. No children allowed. + + Args: + options (List[Option | str | int | float | bool]): A list of values. + **props: Additional properties to be passed to the Select component. + + Returns: + The `create` method is returning an instance of the `Select` class. + """ + converted_options: List[Option] = [] + for option in options: + if not isinstance(option, Option): + converted_options.append(Option(label=str(option), value=option)) + else: + converted_options.append(option) + props["options"] = [o.dict() for o in converted_options] + return super().create(*[], **props) diff --git a/pynecone/components/forms/select.py b/pynecone/components/forms/select.py index 328c4f1fe..fca96825c 100644 --- a/pynecone/components/forms/select.py +++ b/pynecone/components/forms/select.py @@ -1,346 +1,110 @@ -"""Provides a feature-rich Select and some (not all) related components.""" +"""A select component.""" -from typing import Any, Dict, List, Optional, Set, Union +from typing import Any, Dict, List -from pynecone.base import Base -from pynecone.components.component import Component -from pynecone.event import EVENT_ARG +from pynecone.components.component import EVENT_ARG, Component +from pynecone.components.layout.foreach import Foreach +from pynecone.components.libs.chakra import ChakraComponent +from pynecone.components.typography.text import Text +from pynecone.utils import types from pynecone.vars import Var -class Option(Base): - """An option component for the chakra-react-select Select.""" +class Select(ChakraComponent): + """Select component is a component that allows users pick a value from predefined options. Ideally, it should be used when there are more than 5 options, otherwise you might consider using a radio group instead.""" - # What is displayed to the user - label: str - - # The value of the option, must be serializable - value: Any - - # the variant of the option tag - variant: Optional[str] = None - - # [not working yet] - # Whether the option is disabled - # is_disabled: Optional[bool] = None - - # [not working yet] - # The visual color appearance of the component - # options: "whiteAlpha" | "blackAlpha" | "gray" | "red" | - # "orange" | "yellow" | "green" | "teal" | "blue" | "cyan" | - # "purple" | "pink" | "linkedin" | "facebook" | "messenger" | - # "whatsapp" | "twitter" | "telegram" - # default: "gray" - # color_scheme: Optional[str] = None - - # [not working yet] - # The icon of the option tag - # icon: Optional[str] = None - - -class Select(Component): - """The default chakra-react-select Select component. - Not every available prop is listed here, - for a complete overview check the react-select/chakra-react-select docs. - Props added by chakra-react-select are marked with "[chakra]". - """ - - library = "chakra-react-select" tag = "Select" - # Focus the control when it is mounted - auto_focus: Var[bool] + # State var to bind the select. + value: Var[str] - # Remove the currently focused option when the user presses backspace - # when Select isClearable or isMulti - backspace_removes_value: Var[bool] + # The default value of the select. + default_value: Var[str] - # Remove focus from the input when the user selects an option - # (handy for dismissing the keyboard on touch devices) - blur_input_on_select: Var[bool] + # The placeholder text. + placeholder: Var[str] - # When the user reaches the top/bottom of the menu, - # prevent scroll on the scroll-parent - capture_menu_scroll: Var[bool] - - # [chakra] - # To use the chakraStyles prop, first, - # check the documentation for the original styles prop from the react-select docs. - # This package offers an identical API for the chakraStyles prop, however, - # the provided and output style objects use Chakra's sx prop - # instead of the default emotion styles the original package offers. - # This allows you to both use the shorthand styling props you'd normally use - # to style Chakra components, as well as tokens from your theme such as named colors. - # All of the style keys offered in the original package can be used in the chakraStyles prop - # except for menuPortal. Along with some other caveats, this is explained below. - # Most of the components rendered by this package use the basic Chakra component with a few exceptions. - # Here are the style keys offered and the corresponding Chakra component that is rendered: - # - clearIndicator - Box (uses theme styles for Chakra's CloseButton) - # - container - Box - # - control - Box (uses theme styles for Chakra's Input) - # - dropdownIndicator - Box (uses theme styles for Chrakra's InputRightAddon) - # - downChevron - Icon - # - crossIcon - Icon - # - group - Box - # - groupHeading - Box (uses theme styles for Chakra's Menu group title) - # - indicatorsContainer - Box - # - indicatorSeparator - Divider - # - input - chakra.input (wrapped in a Box) - # - inputContainer - Box - # - loadingIndicator - Spinner - # - loadingMessage - Box - # - menu - Box - # - menuList - Box (uses theme styles for Chakra's Menu) - # - multiValue - chakra.span (uses theme styles for Chakra's Tag) - # - multiValueLabel - chakra.span (uses theme styles for Chakra's TagLabel) - # - multiValueRemove - Box (uses theme styles for Chakra's TagCloseButton) - # - noOptionsMessage - Box - # - option - Box (uses theme styles for Chakra's MenuItem) - # - placeholder - Box - # - singleValue - Box - # - valueContainer - Box - chakra_styles: Var[str] - - # Close the select menu when the user selects an option - close_menu_on_select: Var[bool] - - # If true, close the select menu when the user scrolls the document/body. - close_menu_on_scroll: Var[bool] - - # [chakra] - # The visual color appearance of the component - # options: "whiteAlpha" | "blackAlpha" | "gray" | "red" | - # "orange" | "yellow" | "green" | "teal" | "blue" | "cyan" | - # "purple" | "pink" | "linkedin" | "facebook" | "messenger" | - # "whatsapp" | "twitter" | "telegram" - # default: "gray" - color_scheme: Var[str] - - # This complex object includes all the compositional components - # that are used in react-select. If you wish to overwrite a component, - # pass in an object with the appropriate namespace. - # If you only wish to restyle a component, - # we recommend using the styles prop instead. - components: Var[Dict[str, Component]] - - # Whether the value of the select, e.g. SingleValue, - # should be displayed in the control. - control_should_render_value: Var[bool] - - # Delimiter used to join multiple values into a single HTML Input value - delimiter: Var[str] - - # [chakra] - # Colors the component border with the given chakra color string on error state - # default: "red.500" + # The border color when the select is invalid. error_border_color: Var[str] - # Clear all values when the user presses escape AND the menu is closed - escape_clears_value: Var[bool] - - # [chakra] - # Colors the component border with the given chakra color string on focus - # default: "blue.500" + # The border color when the select is focused. focus_border_color: Var[str] - # Sets the form attribute on the input - form: Var[str] - - # Hide the selected option from the menu - hide_selected_options: Var[bool] - - # The id to set on the SelectContainer component. - # id: Var[str] - - # The value of the search input - input_value: Var[str] - - # The id of the search input - input_id: Var[str] - - # Is the select value clearable - is_clearable: Var[bool] - - # Is the select disabled + # If true, the select will be disabled. is_disabled: Var[bool] - # [chakra] - # Style component in the chakra invalid style - # default: False + # If true, the form control will be invalid. This has 2 side effects: - The FormLabel and FormErrorIcon will have `data-invalid` set to true - The form element (e.g, Input) will have `aria-invalid` set to true is_invalid: Var[bool] - # Support multiple selected options - is_multi: Var[bool] - - # [chakra] - # Style component as disabled (chakra style) - # default: False + # If true, the form control will be readonly is_read_only: Var[bool] - # Is the select direction right-to-left - is_rtl: Var[bool] + # If true, the form control will be required. This has 2 side effects: - The FormLabel will show a required indicator - The form element (e.g, Input) will have `aria-required` set to true + is_required: Var[bool] - # Whether to enable search functionality - is_searchable: Var[bool] - - # Minimum height of the menu before flipping - min_menu_height: Var[int] - - # Maximum height of the menu before scrolling - max_menu_height: Var[int] - - # Default placement of the menu in relation to the control. - # 'auto' will flip when there isn't enough space below the control. - # options: "bottom" | "auto" | "top" - menu_placement: Var[str] - - # The CSS position value of the menu, - # when "fixed" extra layout management is required - # options: "absolute" | "fixed" - menu_position: Var[str] - - # Whether to block scroll events when the menu is open - menu_should_block_scroll: Var[bool] - - # Whether the menu should be scrolled into view when it opens - menu_should_scroll_into_view: Var[bool] - - # Name of the HTML Input (optional - without this, no input will be rendered) - name: Var[str] - - # Allows control of whether the menu is opened when the Select is focused - open_menu_on_focus: Var[bool] - - # Allows control of whether the menu is opened when the Select is clicked - open_menu_on_click: Var[bool] - - # Array of options that populate the select menu - options: Var[List[Dict]] - - # Number of options to jump in menu when page{up|down} keys are used - page_size: Var[int] - - # Placeholder for the select value - placeholder: Var[Optional[str]] - - # Marks the value-holding input as required for form validation - required: Var[bool] - - # [chakra] - # If you choose to stick with the default selectedOptionStyle="color", - # you have one additional styling option. - # If you do not like the default of blue for the highlight color, - # you can pass the selectedOptionColorScheme prop to change it. - # This prop will accept any named color from your theme's color palette, - # and it will use the 500 value in light mode or the 300 value in dark mode. - # This prop can only be used for named colors from your theme, not arbitrary hex/rgb colors. - # If you would like to use a specific color for the background that's not a part of your theme, - # use the chakraStyles prop to customize it. - # default: "blue" - selected_option_color_scheme: Var[str] - - # [chakra] - # The default option "color" will style a selected option - # similar to how react-select does it, - # by highlighting the selected option in the color blue. - # Alternatively, if you pass "check" for the value, - # the selected option will be styled like the Chakra UI Menu component - # and include a check icon next to the selected option(s). - # If is_multi and selected_option_style="check" are passed, - # space will only be added for the check marks - # if hide_selected_options=False is also passed. - # options: "color" | "check" - # default: "color" - selected_option_style: Var[str] - - # [chakra] - # The size of the component. - # options: "sm" | "md" | "lg" - # default: "md" - size: Var[str] - - # Sets the tabIndex attribute on the input - tab_index: Var[int] - - # Select the currently focused option when the user presses tab - tab_selects_value: Var[bool] - - # [chakra] - # Variant of multi-select tags - # options: "subtle" | "solid" | "outline" - # default: "subtle" - tag_variant: Var[str] - - # Remove all non-essential styles - unstyled: Var[bool] - - # [chakra] - # If this prop is passed, - # the dropdown indicator at the right of the component will be styled - # in the same way the original Chakra Select component is styled, - # instead of being styled as an InputRightAddon. - # The original purpose of styling it as an addon - # was to create a visual separation between the dropdown indicator - # and the button for clearing the selected options. - # However, as this button only appears when isMulti is passed, - # using this style could make more sense for a single select. - # default: False - use_basic_style: Var[bool] - - # [chakra] - # The variant of the Select. If no variant is passed, - # it will default to defaultProps.variant from the theme for Chakra's Input component. - # If your component theme for Input is not modified, it will be outline. - # options: "outline" | "filled" | "flushed" | "unstyled" - # default: "outline" + # "outline" | "filled" | "flushed" | "unstyled" variant: Var[str] - # How the options should be displayed in the menu. - menu_position: Var[str] = "fixed" # type: ignore + # The size of the select. + size: Var[str] - def get_controlled_triggers(self) -> Dict[str, Var]: + @classmethod + def get_controlled_triggers(cls) -> Dict[str, Var]: """Get the event triggers that pass the component's value to the handler. Returns: A dict mapping the event trigger to the var that is passed to the handler. """ - # A normal select returns the value. - value = EVENT_ARG.value - - # Multi-select returns a list of values. - if self.is_multi: - value = Var.create_safe(f"{EVENT_ARG}.map(e => e.value)", is_local=True) - return {"on_change": value} + return { + "on_change": EVENT_ARG.target.value, + } @classmethod - def get_initial_props(cls) -> Set[str]: - """Get the initial props to set for the component. + def create(cls, *children, **props) -> Component: + """Create a select component. - Returns: - The initial props to set. - """ - return super().get_initial_props() | {"is_multi"} - - @classmethod - def create( - cls, options: List[Union[Option, str, int, float, bool]], **props - ) -> Component: - """Takes a list of options and additional properties, checks if each option is an - instance of Option, and returns a Select component with the given options and - properties. No children allowed. + If a list is provided as the first children, a default component + will be created for each item in the list. Args: - options (List[Option | str | int | float | bool]): A list of values. - **props: Additional properties to be passed to the Select component. + *children: The children of the component. + **props: The props of the component. Returns: - The `create` method is returning an instance of the `Select` class. + The component. """ - converted_options: List[Option] = [] - for option in options: - if not isinstance(option, Option): - converted_options.append(Option(label=str(option), value=option)) - else: - converted_options.append(option) - props["options"] = [o.dict() for o in converted_options] - return super().create(*[], **props) + if len(children) == 1 and isinstance(children[0], list): + children = [Option.create(child) for child in children[0]] + if ( + len(children) == 1 + and isinstance(children[0], Var) + and types._issubclass(children[0].type_, List) + ): + children = [Foreach.create(children[0], lambda item: Option.create(item))] + return super().create(*children, **props) + + +class Option(Text): + """A select option.""" + + tag = "option" + + value: Var[Any] + + @classmethod + def create(cls, *children, **props) -> Component: + """Create a select option component. + + By default, the value of the option is the text of the option. + + Args: + *children: The children of the component. + **props: The props of the component. + + Returns: + The component. + """ + if "value" not in props: + assert len(children) == 1 + props["value"] = children[0] + return super().create(*children, **props)