HTML id attribute for components

Could we have the ability to set the id attribute of a component?

Use Case

I use selenium to automate testing of my apps.

The selectors for components like buttons, links and text boxes can become very cumbersome. They are also brittle and often break if the component moves on the form (e.g. Into a different container).

Options

I can work around this now with some js, but it would be good not to have to do that.

This could be an optional attribute on a component. If I set it, it’s up to me to maintain the uniqueness.

Alternatively, it could just be set on all components using a uuid or similar.

4 Likes

Related older request by another insightful gentleman:

I’m having one of those days today. I even searched here first to see if it already existed!

1 Like

Another one here: Add the component name to the HTML tag

1 Like

A beautifully neat solution from @stucork

Override the base Component class in the startup form/module:

from anvil import Component
from anvil.js import get_dom_node

def set_id(self, value):
    get_dom_node(self).id = value

def get_id(self):
    return get_dom_node(self).id

Component.id = property(get_id, set_id)

Much nicer than the slightly revolting set_id function I’ve been using for years!

6 Likes

Nice and elegant, but I still think the id or name should be there out of the box.

1 Like

WOW. Thank you community for the post I didn’t know I needed so badly…

1 Like

Here’s an example of the sort of thing I end up having to do within a form and would really rather not:

class Form1(Form1Template):
    _identified_components = ["logout_link"]

    def __init__(self, **properties):
        self.init_components(**properties)
        module = self.__class__.__module__.lower()
        for component in self._identified_components:
            getattr(self, component).id = f"{module}.{component}"
3 Likes

At least that is much better/faster/stable than trying to create the most robust xpath :sweat_smile: … the hours I’ve wasted.

3 Likes

By.ID is a whole heap easier!

1 Like

I always wondered, given that IDs already exist in the (computed) web page, if I was going to assign an ID, how the heck would I avoid name collisions? Wouldn’t I need a way to discover all the existing IDs?

Or is there a way to generate a guaranteed unique ID, that won’t conflict with anything currently present, or likely to be added later?

That’s why I use the module name

Makes sense, thank you, Owen.

Unless the module happens to be used multiple times on a form, e.g., in a Repeating Panel or Custom Component?

I wonder if you could mix in the python builtin id( ) function to avoid collisions, in skulpt its just an integer counter that gets incremented from the start of the app on every creation of a new object.

I do my own thing for repeating panels. You can’t rely on anything else always giving the same result for the same component (that I’ve found thus far)

3 Likes

Hello I’ve been linked here from another thread, how exactly does this solution work?

What exactly is the property function and what are its arguments?

And if I do this in the startup module, but then display other modules inside it at runtime, will those modules inherit this adjustment, or do I need to duplicate it across multiple modules?

That property function is the equivalent of defining getter and setter methods on the class using the @property decorator, etc.:

Those modules inherit this adjustment. When you import the same Python module multiple times, it doesn’t import it again, it uses what’s already been imported.

1 Like

Hi all, I came across this and some other associated discussions recently as I was looking to do some test automation myself using Playwright. Quite a few web UI test automation frameworks support looking for alternative element attributes such as data-testid which are separated from the id attribute which should always be unique across the page etc. I wanted to avoid using the id attribute for this reason, and because other web frameworks don’t allow you to control the id.

In Playwrights case there is a specific locator method called page.getByTestId() (see Locators | Playwright) ) which by default looks for the attribute mentioned above, or what ever you have set it to in other config.

I used a solution similar to the above, but I extended Component with a new data_test_id read-only property on startup of my main form. Then in various other places I just set that property.

Here is an example extracts from my code (easy to swap out the attribute name):

from anvil.js import get_dom_node

class FormMain(FormMainTemplate):
  def __init__(self, **properties):
    # Paste the following block into your solution starting form...
    setattr(
        Component,
        'data_test_id',
        property(
            fset=lambda comp, val: get_dom_node(comp).setAttribute('data-testid', val)
        )
    )

    self.init_components(**properties)

, then in one of your other pages/components you can simply do something like:

class Something(SomethingTemplate):
  def __init__(self, **properties):
    self.init_components(**properties)

    # Set the test id value where ever you need to
    self.your_component.data_test_id = 'Something to search by'

Thought I would post this just in case there are other people who want to use an alternative attribute for their testing.

Anvil is awesome BTW!

2 Likes

Playwright sounds like they have the right approach! An attribute is much less disruptive than requiring an ID.

As a general point of style, “monkey-patching” like some of the examples in this thread (ie adding extra properties and things to system classes to do what you want) is something the Python community discourages. It’s brittle, because it could break (or cause mysterious breakages in your code) when we update the inner workings of the Component class. It’s also harder for readers of your code to understand (in this case, they’ll look at the property, go “wait, that’s not built in on all Anvil components, is it?”, try it in their own code, get frustrated, and eventually search over your whole code-base to find the form whose __init__ method installed the monkey patch!).

Instead, the Pythonic way to do it is to use a function. For example, you could put this in a module called TestHooks:

from anvil.js import get_dom_node

def set_test_id(component, test_id):
    get_dom_node(component).setAttribute("data-testid", test_id)

Then in your forms, it’s no more lines of code than your version:

from TestHooks import set_test_id
# ...

class Something(SomethingTemplate):
    def __init__(self, **properties):
        # ...
        set_test_id(self.your_component, "Something to search by")

Just as simple, but much easier for someone else to read – and much less fragile when we update the Component class!

3 Likes

FWIW, I also now use playwright and don’t need this feature request any more!