Input Component

Basic Use

Many web applications require users to fill forms. HTML forms are based on the input HTML tag.

The corresponding JustPy component is Input.

The following program adds a text input field to a page:

import justpy as jp

input_classes = "m-2 bg-gray-200 border-2 border-gray-200 rounded w-64 py-2 px-4 text-gray-700 focus:outline-none focus:bg-white focus:border-purple-500"

async def input_demo(request):
    wp = jp.WebPage()
    in1 = jp.Input(a=wp, classes=input_classes, placeholder='Please type here')
    return wp

jp.justpy(input_demo)

The input Event

The program above is quite boring, it does nothing except allow you to type text into the input field. To make it more interesting, let's have what we type reflected in a paragraph on the page. For this, we need to introduce the input event. When you type, each character typed into the input field generates an input event (yes, the tag and the event are called the same name).

Run the following program:

import justpy as jp

input_classes = "m-2 bg-gray-200 border-2 border-gray-200 rounded w-64 py-2 px-4 text-gray-700 focus:outline-none focus:bg-white focus:border-purple-500"
p_classes = 'm-2 p-2 h-32 text-xl border-2'

async def my_input(self, msg):
    self.div.text = self.value

async def input_demo(request):
    wp = jp.WebPage()
    in1 = jp.Input(a=wp, classes=input_classes, placeholder='Please type here')
    in1.div = jp.Div(text='What you type will show up here', classes=p_classes, a=wp)
    in1.on('input', my_input)
    return wp

jp.justpy(input_demo)

The function input_demo creates a web page and adds an input element called in1 to it (ignore the classes, they are there just to make the input element look nicer and do not affect the functionality of the program).

Notice the placeholder attribute of in1. Before any text is typed into the input field or when it is emptied, the placeholder text is displayed in the field. We then define a div element that is added to the page (using the a keyword argument) and assigned to an attribute of in1. We saw this technique before. It simplifies event handling as we shall see in a second.

Next, we bind the input event of in1 to the function my_input (we could have omitted this line by adding input=my_input as a keyword argument when we created in1). my_input is now the input event handler for in1.

The input event occurs each and every time a character is typed into an input element. After every keystroke this function is run (if the debounce period has expired - see below), and it changes the text of the div to the value of the input field. By assigning the div to an in1 attribute, we have access to all the variables we need in the event handler.

You may have noticed that there is a delay in the updating of the Div. That is because the component by default sets the debounce attribute of the input event to 200ms. This means an input event is generated only after a key has not been pressed for 200ms.

Try holding the a key down and have it repeated. Only when you lift your finger will the Div update. You can set the debounce attribute to the value you prefer in ms, just make sure to take into account the typing speed of your users and the latency of the connection. In general, a higher debounce value means the server will have to handle less communications and that may be an advantage for applications that need to scale.

The change Event and the InputOnlyChange Component

The regular Input component generates an event each time a character is typed into the field. In some case this is not necessary and may put unwanted burden on the server. If you are not implementing a look ahead or validating the field as the user is typing, it is preferable to use InputOnlyChange instead of Input.

InputOnlyChange does not generate the input event, only the change event. The change event is generated when the field loses focus or the user presses the Enter button. It is not generated when the user types a character.

The example below is the same as the one above except that InputOnlyChange is used instead of Input.

import justpy as jp

input_classes = "m-2 bg-gray-200 border-2 border-gray-200 rounded w-64 py-2 px-4 text-gray-700 focus:outline-none focus:bg-white focus:border-purple-500"
p_classes = 'm-2 p-2 h-32 text-xl border-2'

def my_input(self, msg):
    self.div.text = self.value

def input_demo(request):
    wp = jp.WebPage()
    in1 = jp.InputChangeOnly(a=wp, classes=input_classes, placeholder='Please type here')
    in1.div = jp.Div(text='What you type will show up here only when Input element loses focus or you press Enter',
                     classes=p_classes, a=wp)
    in1.on('input', my_input)
    in1.on('change', my_input)
    return wp

jp.justpy(input_demo)

The Type Attribute

Number Example

The input component can be of different types such as 'number' or 'password'.

import justpy as jp

input_classes = "m-2 bg-gray-200 border-2 border-gray-200 rounded w-64 py-2 px-4 text-gray-700 focus:outline-none focus:bg-white focus:border-purple-500"
p_classes = 'm-2 p-2 h-32 text-xl border-2'

async def my_input(self, msg):
    self.div.text = self.value

async def input_demo(request):
    wp = jp.WebPage()
    in1 = jp.Input(type='number', a=wp, classes=input_classes, placeholder='Please type here')
    in1.div = jp.Div(text='What you type will show up here', classes=p_classes, a=wp)
    in1.on('input', my_input)
    return wp

jp.justpy(input_demo)

In the example above the type of in1 is set to 'number'. Run the program and verify that only numbers can be input into the element.

Color Example

The Input component also allows you to choose colors.

import justpy as jp

def color_demo(request):
    wp = jp.WebPage()
    in1 = jp.Input(type='color', a=wp, classes='m-2 p-2', style='width: 100px; height: 100px', input=color_change, debounce=30)
    in1.d = jp.Div(text='Click box above to change color of this text', a=wp, classes='border m-2 p-2 text-2xl font-bold')
    return wp

def color_change(self, msg):
    self.d.style=f'color: {self.value}'
    self.d.text = f'The color of this text is: {self.value}'

jp.justpy(color_demo)

Radio Button Example

In the example below we create two sets of radio buttons.

Later in the tutorial we will see how to create a component for radio buttons which is a more intuitive way to use them.

Tip

If you plan to use Quasar with JustPy, checkout the QOptionGroup component

import justpy as jp

def radio_test():
    wp = jp.WebPage()
    genders = ['male', 'female', 'other']
    ages = [(0, 30), (31, 60), (61, 100)]

    outer_div = jp.Div(classes='border m-2 p-2 w-64', a=wp)

    jp.P(a=outer_div, text='Please select your gender:')
    for gender in genders:
        label = jp.Label(classes='inline-block mb-1 p-1', a=outer_div)
        radio_btn = jp.Input(type='radio', name='gender', value=gender, a=label)
        jp.Span(classes='ml-1', a=label, text=gender.capitalize())

    jp.Div(a=outer_div, classes='m-2')  # Add spacing and line break
    jp.P(a=outer_div, text='Please select your age:')
    for age in ages:
        label = jp.Label(classes='inline-block mb-1 p-1', a=outer_div)
        radio_btn = jp.Input(type='radio', name='age', value=age[0], a=label)
        jp.Span(classes='ml-1', a=label, text=f'{age[0]} - {age[1]}')
        jp.Br(a=outer_div)

    return wp

jp.justpy(radio_test)

Radio buttons generate the 'change' event on all browsers. They generate the 'input' event just on some. When one is checked the 'change' event is generated.

When one radio button is checked, JustPy automatically un-checks the other radio buttons in the group.

Radio buttons are grouped according to their name attribute and the value of their form attribute if one is assigned. Buttons with the same name on different assigned forms, will be in different groups. All radio buttons not in any form but with the same name, will be in one group.

Note

You explicitly need to specify the form the buttons are on using the form attribute if you want to give button groups in different forms the same name. This is because JustPy does not know which form the button will be added to or has been added to. Alternatively, just have a unique name for each button group on the page.

In the example below, the results of clicking a radio button are shown using the event handler radio_changed. Notice that the value of the radio button is always the same. What changes is its checked property. The value of a group of radio buttons is the value of the radio button in the group that is checked.

To make all the radio buttons in the group available to the event handler, when we create them, we also create a list that holds all the radio buttons in the group. We assign this list to an attribute of each radio button element (in our case btn_list). In the event handler we iterate over this list to report which radio button is pressed.

import justpy as jp


def radio_changed(self, msg):
    self.result_div.text = ''
    d = jp.Div(a=self.result_div, classes='m-2 p-2 border')
    for btn in self.btn_list:
        if btn.checked:
            jp.Span(text=f'{btn.value} is checked', a=d, classes='text-green-500 mr-6')
        else:
            jp.Span(text=f'{btn.value} is NOT checked', a=d, classes='text-red-500 mr-6')


def radio_test():
    wp = jp.WebPage()
    genders = ['male', 'female', 'other']
    ages = [(0, 30), (31, 60), (61, 100)]

    outer_div = jp.Div(classes='border m-2 p-2 w-64', a=wp)
    # Create div to show radio button selection but don't add yet to page. It will be added at the end
    # It is created here so that it could be assigned to the radio button attribute result_div
    result_div = jp.Div(text='Click radio buttons to see results here', classes='m-2 p-2 text-xl')

    jp.P(a=outer_div, text='Please select your gender:')
    gender_list = []
    for gender in genders:
        label = jp.Label(classes='inline-block mb-1 p-1', a=outer_div)
        radio_btn = jp.Input(type='radio', name='gender', value=gender, a=label, btn_list=gender_list,
                             result_div=result_div, change=radio_changed)
        gender_list.append(radio_btn)
        jp.Span(classes='ml-1', a=label, text=gender.capitalize())

    jp.Div(a=outer_div, classes='m-2')  # Add spacing and line break

    jp.P(a=outer_div, text='Please select your age:')
    age_list = []
    for age in ages:
        label = jp.Label(classes='inline-block mb-1 p-1', a=outer_div)
        radio_btn = jp.Input(type='radio', name='age', value=age[0], a=label, btn_list=age_list,
                             result_div=result_div, change=radio_changed)
        age_list.append(radio_btn)
        jp.Span(classes='ml-1', a=label, text=f'{age[0]} - {age[1]}')
        jp.Br(a=outer_div)

    wp.add(result_div)
    return wp

jp.justpy(radio_test)

Checkbox Example

Below is an example of a checkbox and a textbox connected using the model attribute (you may skip this for now and return to this example after completing the chapter describing the model attribute in this tutorial).

import justpy as jp

def check_test():
    wp = jp.WebPage(data={'checked': True})
    label = jp.Label(a=wp, classes='m-2 p-2 inline-block')
    c = jp.Input(type='checkbox', classes='m-2 p-2 form-checkbox', a=label, model=[wp, 'checked'])
    caption = jp.Span(text='Click to get stuff', a=label)

    in1 = jp.Input(model=[wp, 'checked'], a=wp, classes='border block m-2 p-2')
    return wp

jp.justpy(check_test)

See what happens when you clear the text input element.

Changing Focus using Keyboard Events

In the example below, Input elements respond to the Esc and Enter keys. When Esc is pressed, the field value is set to the empty string. When Enter is pressed, focus moves to the next field down unless it is the last field in which case focus is moved to the first.

To make the example work, an event handler for the 'blur' event is required. When one of the input elements loses focus, the blur event occurs and the focus field of the element is set to false so that the correct element will have focus.

By setting the set_focus attribute of an element to True, you transfer the focus to it. If the attribute is True for multiple elements on the page, the results are unpredictable and therefore the blur event handler is required to make sure the attribute is True for only one element.

import justpy as jp

def my_blur(self, msg):
        self.set_focus = False

def key_down(self, msg):
    # print(msg.key_data)
    key = msg.key_data.key
    if key=='Escape':
        self.value=''
        return
    if key=='Enter':
        self.set_focus = False
        try:
            next_to_focus = self.input_list[self.num + 1]
        except:
            next_to_focus = self.input_list[0]
        next_to_focus.set_focus = True
        return
    return True  # Don't update the page


def focus_test():
    wp = jp.WebPage()
    d = jp.Div(classes='flex flex-col  m-2', a=wp, style='width: 600 px')
    input_list = []
    number_of_fields = 5
    for i in range(1, number_of_fields + 1):
        label = jp.Label( a=d, classes='m-2 p-2')
        jp.Span(text=f'Field {i}', a=label)
        in1 = jp.Input(classes=jp.Styles.input_classes, placeholder=f'{i} Type here', a=label, keydown=key_down, spellcheck="false")
        in1.on('blur', my_blur)
        in1.input_list = input_list
        in1.num = i - 1
        input_list.append(in1)
    return wp

jp.justpy(focus_test)

As a rule of thumb, try to limit the usage of keyboard events to a minimum since they don't work well for mobile users.

Using Select

The select tag needs to be used together with the option tag. In JustPy these correspond to Select and Option elements.

The program below creates a select element whose value changes the background color of a Div.

import justpy as jp


def change_color(self, msg):
    self.color_div.set_class(f'bg-{self.value}-600')


def comp_test():
    wp = jp.WebPage()
    colors = ['red', 'green', 'blue', 'pink', 'yellow', 'teal', 'purple']
    select = jp.Select(classes='w-32 text-xl m-4 p-2 bg-white  border rounded', a=wp, value='red',
                  change=change_color)
    for color in colors:
        select.add(jp.Option(value=color, text=color, classes=f'bg-{color}-600'))
    select.color_div = jp.Div(classes='bg-red-600 w-32 h-16 m-4',a=wp)
    return wp

jp.justpy(comp_test)

Your First Component

JustPy allows building your own reusable components. We will have a lot more to say about this later, but just to start easing into the subject, let's suppose we want to encapsulate the functionality of an Input coupled to a Div into one component (like in one of the first examples above). The program would look like this:

import justpy as jp

class InputWithDiv(jp.Div):

    input_classes = "m-2 bg-gray-200 border-2 border-gray-200 rounded w-64 py-2 px-4 text-gray-700 focus:outline-none focus:bg-white focus:border-purple-500"

    @staticmethod
    def input_handler(self, msg):
        self.div.text = self.value

    def __init__(self, **kwargs):
        super().__init__(**kwargs)

        self.in1 = jp.Input(a=self, classes=self.input_classes, placeholder='Please type here', input=self.input_handler)
        self.in1.div = jp.Div(text='What you type will show up here', classes='m-2 p-2 h-32 text-xl border-2', a=self)


def input_demo(request):
    wp = jp.WebPage()
    for i in range(10):
        InputWithDiv(a=wp)
    return wp

jp.justpy(input_demo)

Try running the program. It will put on the page 10 pairs of input and div elements. If you type into the respective input field, the text will show up in the respective div.

JustPy components are Python classes that inherit from JustPy classes. In the example above, we define the class InputWithDiv which inherits from the JustPy class Div. In the __init__ method an input element and another div is added to the basic div, with the appropriate functionality. Now, we have a component, InputWithDiv, that we can reuse as we please in any project.

If you don't completely understand what is going on, don't worry. We will revisit this in much more detail later. The take home message at this stage is that the way you build complex applications in JustPy is by building components with isolated functionality. Hopefully, if JustPy gains popularity, there will be many components that the community will develop and share.