StimulusJS Target is undefined but .querySelector works

I’m confused. I’m getting some inconsistent behaviour with a target in a stimulus controller.

Using StimulusJS via importmap pin "@hotwired/stimulus", to: "stimulus.min.js", preload: true

I have a basic form with stimulus controller.

<%= form_with model: @message, data: { controller: "message-form" } do |form| %>

  <%= form.file_field :attachments, class: 'file-input', id: 'file-input', multiple: true, hidden: "hidden",
                      data: { message_form_target: "attachmentInput" } %>

  <i class="fa-solid fa-paperclip fa-lg" data-action="click->message-form#openAttachments"></i>

<% end %>
import {Controller} from "@hotwired/stimulus"

export default class extends Controller {
    static targets = ["messageInput", "sendBtn", "attachmentInput"]

    connect() {
        console.log(this.attachmentInputTarget) // Outputs HTML element

        setInterval(this.hideAttachments, 5000)

        let attachmentInput = document.querySelector('#file-input')
        console.log(this.attachmentInputTarget) // Undefined
        console.log(attachmentInput) // Outputs HTML element
        console.log(document.getElementById('file-input') == this.attachmentInputTarget) // false

        if (!document.getElementById('file-input').files[0]) {
            attachmentInput.setAttribute("hidden", "hidden")
        } else {

So, attatchmentInputTarget in connect() acts as I would expect it to, but when hideAttachments is called attatchmentInputTarget is undefined.

Okay, as I wrote this all out I think I’ve figured it out.

Because of the delayed call with setInterval, hideAttachments gets called outside of the stimulus controller as vanilla JS, so has no reference to the Target.

Correct. You want to include .bind(this) in order to keep the scope.