Ludovic Frank - Freelance developer

Stimulus and intl-tel-input without exploding CPU load

ionicons-v5-k Ludovic Frank May 19, 2022
134 reads Level: Beginner

When I was writing an application using Stimulus and needed to use intl-tel-input, I came across this "Github issue".

What's going on between Stimulus and intl-tel-input?

In fact, to work, intl-tel-input clones the base element, so if you link this base element to a Stimulus controller, and it's this controller that instantiates intl-tel-input, you'll need to use intl-tel-input.If you link this base element to a Stimulus controller, and it's this controller that instantiates intl-tel-input, then you're simply entering an infinite loop until you completely exhaust the memory space allowed to you by the browser engine.

In the "Github issue", DHH says "If you create an infinite loop, it's hard to counter".

How can we make it work anyway?

We'll simply "cheat", as usual, we know that the base element can't be used to trigger a Stimulus controller, but an element above it, it's possible isn't it?

The idea is very simple: we surround our "input" with a div, and link this div to the Stimulus controller. Then our "input" is simply a "target", and when intl-tel-input does what it's supposed to do, there's no need to recreate another Stimulus controller.

To do this in your template (in my case, a twig)

<div class="col-12 col-md-6">
<div class="col-12">
{{ form_label(form.tmpPhone) }}
</div>
<div class="col-12" {{ stimulus_controller('intl-tel-input' , {
'locale' : app.request.locale,
'currentNumber': reservation.phone
}) }}>
{{ form_widget(form.tmpPhone) }}
</div>
</div>

Simply place the Stimulus controller on the div surrounding your input.

Then put an attribute on the "input", in my case in a Symfony form.

<?php
namespace App\Form;
class MyGreatForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('tmpPhone', TextType::class, [
'required' => true,
'attr' => [
'data-intl-tel-input-target' => 'input',
'autocomplete' => 'tel'
]
]);
}
}

(Yes, I've added a little "autocomplete" too, isn't that nice?)

Finally, here's an example of my Stimulus controller

import {Controller} from '@hotwired/stimulus'
import intlTelInput from 'intl-tel-input';
export default class extends Controller {
static targets = ['input'];
static values = {
currentNumber: String
};
connect() {
let localizedCountries = {};
this.intlInput = intlTelInput(this.inputTarget, {
utilsScript: '/static/intl-tel-input-utils.js',
hiddenInput: 'phone',
preferredCountries: ['fr', 'lu', 'be', 'de', 'ch'],
localizedCountries
});
if (this.currentNumberValue !== '') {
this.intlInput.setNumber(this.currentNumberValue)
}
}
disconnect() {
this.intlInput.destroy();
super.disconnect();
}
}

Nothing special, except that you never work with "this.element".

Conclusion

Seeing the "outcome", I thought it might be a good idea to do a quick article on this problem, so I did.
Until next time?