Copy a custom component created with personalized HTML

I created a custom component with personalized HTML and Javascript. The component is a widget and when the user interact it an event is generated and managed with javascript. The a python function in Anvil Form is called. The widget has an element with an ID that I use as reference in my JS code.

When I convert the Form in component and use it multiple times in another Form (creating a copy), only the fist is shown, this happen because the ID of my HTML elements maintain the same name.
Is there a way to duplicate the component? So I create a template (HTML+JS) and then I can insert it multiple times in my final App? Like a random number concatenated to to my IDs and generated by Anvil when creating the component instance?

Here’s one way. This is a custom component which can be added to a project as a dependency. Each instance requires a unique ID, so I just replace the ID in the DOM :

<div id="unique_table_div"></div>

<script>

  	/*
    Changes the DIV to a random one to avoid clashes when multiple 
    instances are created.
    Uses the same random id in the global list of tabulator tables.
    */
  	$(document).ready(function(){
        let unique_id = "uid_"+getRandomIntInclusive(1,9999999).toString();
		let div_id = 'unique_table_div';
      
        if ( document.getElementById(div_id) ) {
          	document.getElementById(div_id).setAttribute("id",unique_id);
        }
      
 	    window['tableArray'][unique_id] = new MyTable("#"+unique_id,$("#"+unique_id));

      	/*
        Call home to register the unique ID
        */
      	anvil.call($("#"+unique_id), "table_created",unique_id);  
            
    });

</script>

Have you tried adding a property to the custom component, adding a textbox, making it invisible, assigning the property value to the textbox text and using jquery to find the textbox using its inner html?
If this works (I’m on my cell now, I haven’t tried) you will be able to set the property for each instance of the component in the ide.
Please let me know if you want a more detailed explanation (and a verification that my idea will actually work) and I will try tomorrow.

I used the approach proposed by David, it’s working great! But, I implemented the code using Brython instead of Javascript. This is an example of a DevExtreme widget integration (a simple button, but applicable to other JS/HTML/CSS widgets)

<center style="font-style:italic; color:#888;">
  <div id="buttonContainer" class="widget1"></div>
</center>

<script type="text/python">
import random
from browser import document, alert, window

jq = window.jQuery

def generate_rand_ID():
  return "".join([random.choice("abcdefghijklmnopqrstuvwxyz") for i in range(8)])
	
unique_IDs = ['buttonContainer']	
random_ID = generate_rand_ID()
postfix = "-inst" + random_ID
for id in unique_IDs:
  el = document.getElementById(id)
  if el:
    el.attrs['id'] = el.attrs['id'] + postfix
window.anvil.call(el, "save_ref_ID", random_ID)
   		

def buttonContainer_onClick_result(res):
  alert(res)

def ready(ev):
  jq('#buttonContainer'+postfix).dxButton({
    'text': "Click me!",
    'onClick': lambda ev: window.anvil.call(ev.element, "my_method", "World").then(buttonContainer_onClick_result)
  })
    
jq(ready)
</script>

In the form Class I have then

  def form_show(self, **event_args):
    """This method is called when the HTML panel is shown on the screen"""
    self.call_js("brython", 1)

    
  def my_method(self, name):
    alert("Hello, %s!" % name)
    return "Returned Message"
  
  
  def save_ref_ID(self, random_id):
    self.ref_ID = random_id

And in Native Libraries I imported the Brython interpreter and the Widget library (+CSS). Note that the DevExtreme library is free only for non commercial uses.

<script type="text/javascript"
    src="https://cdnjs.cloudflare.com/ajax/libs/brython/3.7.5/brython.min.js">
</script>
<script type="text/javascript"
    src="https://cdnjs.cloudflare.com/ajax/libs/brython/3.7.5/brython_stdlib.js">
</script>

<!-- DevExtreme library -->
<script type="text/javascript" src="https://cdn3.devexpress.com/jslib/19.1.6/js/dx.all.js"></script>
<link rel="stylesheet" href="https://cdn3.devexpress.com/jslib/19.1.6/css/dx.common.css">
<link rel="stylesheet" href="https://cdn3.devexpress.com/jslib/19.1.6/css/dx.light.css">
1 Like

It may help to know that this refers to the current Form. So you can select the element with class bob using $(this).find('.bob')[0]:

<input class="bob" placeholder="Hi, I'm Bob"></input>

<script>
  var getBob = function() {
    bob = $(this).find('.bob')[0];
    return bob.value;
  }
</script>

Here’s an example where I’ve got two instances of the same Custom HTML Form, and each one selects its own input box:

hi-im-bob

(Of course, you would normally use an Anvil TextBox rather than using HTML for something so simple!)

The Button’s click handler is simply:

  def button_1_click(self, **event_args):
    """This method is called when the button is clicked"""
    self.label_result.text = self.call_js('getBob')

Here’s a clone link for that app:

https://anvil.works/build#clone:QVVDETJWBHYIKJPW=3XAPYZQH4W4T44VP4VZ4SKT3

1 Like

Always had trouble getting my head around this, but …

I have a custom component form that initialises an external JS widget on a unique div ID (as the widget’s docs say I must). If I want to have multiple instances of this custom component on the same Anvil form, then each ID has to be globally unique, yes?

That’s been my reasoning behind my random ID functions I referred to in this thread.

Your example @shaun is using dom classes instead of IDs. I assume I cannot ā€œburyā€ duplicate IDs inside a class instance? I know the answer to that, I’m sure.

Is there a better way than mine when you require IDs for the widgets?

Also interested in this, as I’ve been using the unique IDs approach (since I need to address the components from Python). If there’s a simpler way, that’d be great.

@david.wylie see comments below! :smiley:

as the widget’s docs say I must

Do you mean the Anvil docs? Sounds like a bit that might need improving, do you remember where?

If I want to have multiple instances of this custom component on the same Anvil form, then each ID has to be globally unique, yes?

Yes, you shouldn’t use duplicate IDs in any HTML according to the HTML spec.

Your example @shaun is using dom classes instead of IDs. I assume I cannot ā€œburyā€ duplicate IDs inside a class instance?

Can you describe more specifically how this would work? I’m not quite picturing it.

Is there a better way than mine when you require IDs for the widgets?

What are you using the IDs for? (Assuming you’re able to divulge!)

The $(this).find('.bob')[0] approach might be the way to go - if you need to select the element from outside, you might be able to write an accessor function in the Custom HTML Form (@jshaffstall - that may work for you too?) .

No. The widget I’m using (http://tabulator.info) needs to be attached to a div id rather than a class. At least I think it does, as you pass the id to the constructor rather than the dom object itself. This is from one of their examples :

<div id="example-table"></div>
...
var table = new Tabulator("#example-table", {.. etc .

Ignore that bit - makes no sense :slight_smile:

So if I am trying to have several of these on a page, I seem to have to ensure there is an empty div with a globally unique id.

Looking at the Tabulator docs, it seems that you can initialise one from a JQuery selector:

// From the Tabulator documentation
$("#example-table").tabulator();

So you could do this in your Custom HTML Form:

<div class="tabulate"></div>

<script>
  function initTabulator() {
    $(this).find('.tabulate')[0].tabulator();
  }

(Or even use JQuery’s each function to iterate over all the elements found by find, that way you can use multiple <div class="tabulate"> on one Form.

Assume you mean :

<div class="tabulate"></div>

?

Yep, thanks for that :smiley: (original post edited)

1 Like

I read the docs many times over, yet that did not occur to me.

I will change my component and see if that works. Cheers.

1 Like

I’ve looked at your example from above. I think I understand how it’s working, with $(this) restricting the search to the current component, right? So when it finds items of class ā€œbobā€, it’s only returning the ones in the current component.

And on the Anvil side, when it calls self.call_js(ā€˜getBob’) Anvil makes sure $(this) is set to the right component.

If I’m understanding that correctly, I should be able to simplify my code and reduce the Javascript clutter quite a bit. Thanks for the example!

How would you recommend I reference the newly created component?

I was sending the unique id back to Anvil for that purpose. A single form might have 4 of these components, so I maintained my own ā€œnamespaceā€ array of live components. When I wanted to call the api of s specific one I would use this id.

@david.wylie: $(this).find('.tabulate') returns a JavaScript array, so you can get the nth one on the page (starting at the top) using $(this).find('.tabulate')[n].

@jshaffstall: That’s correct!

It required a bit of reworking (that example you posted @shaun was from an older version), but it now works without renaming the DIV, and I think is a much better control for it.

I’ll post it for general consumption once I’ve tidied it all up and got rid of my debug.

2 Likes

A post was split to a new topic: Custom component with canvas