Django - javascript - ajax does not work with javascript created lines
I have three classes lets say,
class Item(models.Model):
id = models.AutoField(primary_key=True) # Explicitly define the id column as the primary key
itemref = models.IntegerField(blank=True, null=True)
code = models.CharField(max_length=150, db_collation='Turkish_CI_AS', blank=True, null=True)
tanimlama = models.CharField(max_length=150, db_collation='Turkish_CI_AS', blank=True, null=True)
itemname = models.CharField(max_length=150, db_collation='Turkish_CI_AS', blank=True, null=True)
class SAFiche(models.Model):
nothing important
class SALine(models.Model):
safiche = models.ForeignKey(SAFiche, on_delete=models.CASCADE)
item = models.ForeignKey('accounting.Item', on_delete=models.CASCADE)
In views, I have ajax for autocomplate:
def item_autocomplete(request):
query = request.GET.get('q', '')
items = Item.objects.filter(
Q(code__icontains=query) |
Q(itemname__icontains=query) |
Q(tanimlama__icontains=query)
)[:10] # Limit to 10 results
results = []
for item in items:
label = f"{item.code or ''} - {item.itemname or item.tanimlama or ''}"
results.append({
'id': item.id,
'label': label.strip(' -'),
})
return JsonResponse(results, safe=False)
I properly adjusted rest. This is my forms:
class SALineCreateForm(forms.ModelForm):
urun_search = forms.CharField(
label="Ürün Ara",
required=False,
widget=forms.TextInput(attrs={
'placeholder': 'Ürün kodu veya adı...',
'class': 'item-search-input border rounded p-2 w-full',
'autocomplete': 'off'
})
)
class Meta:
model = SALine
fields = [
'urun', 'miktar'
]
widgets = {
'urun': forms.HiddenInput(),
'miktar': forms.NumberInput(attrs={'class': 'w-full form-input rounded-md'}),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# If editing an existing line, fill the search box with the item name
if getattr(self.instance, 'urun_id', None):
self.fields['urun_search'].initial = str(self.instance.urun)
SALineCreateFormSet = inlineformset_factory(
SAFiche,
SALine,
form=SALineCreateForm,
extra=1,
can_delete=True
)
Autocomplating works well with forms created inlines. However, when I use javascript with create new lines button, urun field does auto complate. Nothing happens on the console if I write something to newly created lines urun fields. What can be the problem? How can I solve it?
urun means item in turkish.
This is how I add new item lines:
<script>
document.addEventListener('DOMContentLoaded', () => {
const formsetContainer = document.getElementById('formset-container');
const emptyFormTemplate = document.getElementById('empty-form-template').innerHTML;
const addProductButton = document.getElementById('add-product');
addProductButton.addEventListener('click', () => {
// Get current form count from the management form
const totalFormsInput = document.querySelector('#id_saline_set-TOTAL_FORMS');
const totalForms = parseInt(totalFormsInput.value, 10);
// Create a new form from the empty template
const newFormHTML = emptyFormTemplate.replace(/__prefix__/g, totalForms);
formsetContainer.insertAdjacentHTML('beforeend', newFormHTML);
// Update the TOTAL_FORMS count
totalFormsInput.value = totalForms + 1;
});
});
</script>
And this is rest of the js code which handles autocomplate:
<script>
document.addEventListener('DOMContentLoaded', () => {
const firmSearchInput = document.getElementById("id_firm_search");
const firmHiddenInput = document.getElementById("id_firm");
const resultsContainer = document.getElementById("firm-search-results");
let selectedIndex = -1; // To track the currently highlighted index
// Fetch data from the firm_autocomplete endpoint
async function fetchFirms(query) {
if (!query.trim()) {
resultsContainer.innerHTML = "";
resultsContainer.classList.add("hidden");
return;
}
try {
const response = await fetch("{% url 'operations:firm_autocomplete' %}?q=" + encodeURIComponent(query));
const data = await response.json();
renderResults(data);
} catch (err) {
console.error("Autocomplete error:", err);
}
}
// Render the list of firm suggestions
function renderResults(firms) {
if (!firms.length) {
resultsContainer.innerHTML = "";
resultsContainer.classList.add("hidden");
return;
}
selectedIndex = -1; // Reset selected index
resultsContainer.innerHTML = firms
.map((firm, index) => `
<div class="px-3 py-2 hover:bg-gray-100 cursor-pointer"
data-index="${index}"
data-firm-id="${firm.id}"
data-firm-label="${firm.label}">
${firm.label}
</div>
`).join('');
resultsContainer.classList.remove("hidden");
// Add click handlers for each suggestion
const items = resultsContainer.querySelectorAll("[data-firm-id]");
items.forEach((item, index) => {
item.addEventListener("click", () => selectFirm(index));
});
}
// Select a firm by index
function selectFirm(index) {
const items = resultsContainer.querySelectorAll("[data-firm-id]");
if (index >= 0 && index < items.length) {
const selectedFirm = items[index];
const selectedFirmId = selectedFirm.getAttribute("data-firm-id");
const selectedFirmLabel = selectedFirm.getAttribute("data-firm-label");
// Update hidden input with the firm ID
firmHiddenInput.value = selectedFirmId;
// Update the visible text box
firmSearchInput.value = selectedFirmLabel;
// Hide the dropdown
resultsContainer.innerHTML = "";
resultsContainer.classList.add("hidden");
}
}
// Trigger fetch on input
firmSearchInput.addEventListener("input", (e) => {
const query = e.target.value;
fetchFirms(query);
});
// Keyboard navigation
firmSearchInput.addEventListener("keydown", (e) => {
const items = resultsContainer.querySelectorAll("[data-firm-id]");
if (e.key === "ArrowDown") {
// Move focus down
e.preventDefault();
selectedIndex = (selectedIndex + 1) % items.length;
highlightItem(items, selectedIndex);
} else if (e.key === "ArrowUp") {
// Move focus up
e.preventDefault();
selectedIndex = (selectedIndex - 1 + items.length) % items.length;
highlightItem(items, selectedIndex);
} else if (e.key === "Enter") {
// Select the currently highlighted item
e.preventDefault();
if (selectedIndex >= 0) {
selectFirm(selectedIndex);
}
} else if (e.key === "Tab" && selectedIndex >= 0) {
// Select and move to the next form element on Tab
selectFirm(selectedIndex);
}
});
// Highlight a specific item
function highlightItem(items, index) {
items.forEach((item, i) => {
if (i === index) {
item.classList.add("bg-gray-100");
} else {
item.classList.remove("bg-gray-100");
}
});
}
// Hide dropdown when clicking outside
document.addEventListener("click", (e) => {
if (!resultsContainer.contains(e.target) && e.target !== firmSearchInput) {
resultsContainer.innerHTML = "";
resultsContainer.classList.add("hidden");
}
});
});
</script>
<script>
document.addEventListener('DOMContentLoaded', () => {
// 1. For each row in the formset, we have an input with class "item-search-input"
const itemSearchInputs = document.querySelectorAll('.item-search-input');
itemSearchInputs.forEach((input, index) => {
const resultsContainerId = `item-search-results-${index}`;
const resultsContainer = document.getElementById(resultsContainerId);
const hiddenUrunInput = input
.closest('.formset-row')
.querySelector('input[name$="-urun"]');
let selectedIndex = -1; // Tracks the currently highlighted index
// Autocomplete fetch function
async function fetchItems(query) {
if (!query.trim()) {
resultsContainer.innerHTML = '';
resultsContainer.classList.add('hidden');
return;
}
try {
const url = "{% url 'operations:item_autocomplete' %}?q=" + encodeURIComponent(query);
const response = await fetch(url);
const data = await response.json();
renderResults(data);
} catch (err) {
console.error("Item autocomplete error:", err);
}
}
// Renders the dropdown results
function renderResults(items) {
if (!items.length) {
resultsContainer.innerHTML = '';
resultsContainer.classList.add('hidden');
return;
}
selectedIndex = -1; // Reset selected index
resultsContainer.innerHTML = items.map((item, i) => `
<div class="px-3 py-2 hover:bg-gray-100 cursor-pointer"
data-index="${i}"
data-item-id="${item.id}"
data-item-label="${item.label}">
${item.label}
</div>
`).join('');
resultsContainer.classList.remove('hidden');
// Click handlers
const suggestionDivs = resultsContainer.querySelectorAll('[data-item-id]');
suggestionDivs.forEach(div => {
div.addEventListener('click', () => {
selectItem(div);
});
});
}
// Handle item selection
function selectItem(element) {
const selectedId = element.getAttribute('data-item-id');
const selectedLabel = element.getAttribute('data-item-label');
// Set hidden input to item id, visible input to label
hiddenUrunInput.value = selectedId;
input.value = selectedLabel;
// Hide the dropdown
resultsContainer.innerHTML = '';
resultsContainer.classList.add('hidden');
}
// Highlight a specific item
function highlightItem(items, index) {
items.forEach((item, i) => {
if (i === index) {
item.classList.add('bg-gray-100');
} else {
item.classList.remove('bg-gray-100');
}
});
}
// Listen for typing
input.addEventListener('input', (e) => {
fetchItems(e.target.value);
});
// Listen for keyboard events
input.addEventListener('keydown', (e) => {
const items = resultsContainer.querySelectorAll('[data-item-id]');
if (e.key === 'ArrowDown') {
e.preventDefault();
if (items.length > 0) {
selectedIndex = (selectedIndex + 1) % items.length;
highlightItem(items, selectedIndex);
}
} else if (e.key === 'ArrowUp') {
e.preventDefault();
if (items.length > 0) {
selectedIndex = (selectedIndex - 1 + items.length) % items.length;
highlightItem(items, selectedIndex);
}
} else if (e.key === 'Enter') {
e.preventDefault();
if (selectedIndex >= 0 && selectedIndex < items.length) {
selectItem(items[selectedIndex]);
}
} else if (e.key === 'Tab') {
if (selectedIndex >= 0 && selectedIndex < items.length) {
selectItem(items[selectedIndex]);
}
}
});
// Optional: hide dropdown if user clicks outside
document.addEventListener('click', (e) => {
if (!resultsContainer.contains(e.target) && e.target !== input) {
resultsContainer.innerHTML = '';
resultsContainer.classList.add('hidden');
}
});
});
});
</script>