JavaScript klijent za Web API: prikaz, dodavanje, izmena i brisanje

Lekcija 5 od 7 · ASP.NET Core

API vraća samo podatke; korisniku treba stranica. Pravimo je u HTML-u, Bootstrap-u i JavaScript-u: tabela proizvoda, forma za dodavanje i izmenu i dugmad za brisanje – sve preko zahteva GET, POST, PUT i DELETE ka API-ju iz prethodnih lekcija.

Pre nego što počneš: potrebno predznanje – Web API i baza, DOM i događaji.
Kako da učiš: radi u svom projektu korak po korak i posle svake izmene pokreni aplikaciju; zadatak na kraju prvo uradi sam/a. Kod je u C#-u za .NET 8 ili noviji (preporuka: aktuelna LTS verzija); naziv fajla je u komentaru na vrhu svakog primera.

Gde je klijent

NačinKakoCORS
u istoj aplikaciji (ova lekcija)fajlovi u wwwroot, u Program.cs UseDefaultFiles + UseStaticFiles (lekcija 4)nije potreban
posebna aplikacija na drugom portu ili domenunpr. Live Server na localhost:5500server mora da dozvoli taj izvor (ispod)
mobilna ili desktop aplikacijaisti HTTP zahtevine odnosi se na njih (CORS je pravilo pregledača)

Stranica

<!DOCTYPE html>
<html lang="sr">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Prodavnica – proizvodi</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
</head>
<body>
  <div class="container py-4">
    <h1 class="h3">Proizvodi</h1>

    <form id="forma" class="row g-2 align-items-end mb-3">
      <input type="hidden" id="id">
      <div class="col-12 col-md-5"><label class="form-label" for="naziv">Naziv</label>
        <input class="form-control" id="naziv" required maxlength="50"></div>
      <div class="col-6 col-md-2"><label class="form-label" for="cena">Cena (din)</label>
        <input class="form-control" id="cena" type="number" min="0" required></div>
      <div class="col-6 col-md-2"><label class="form-label" for="kolicina">Količina</label>
        <input class="form-control" id="kolicina" type="number" min="0" required></div>
      <div class="col-12 col-md-3 d-flex gap-2">
        <button class="btn btn-primary flex-fill" id="sacuvaj">Dodaj</button>
        <button class="btn btn-outline-secondary" type="button" id="odustani" hidden>Odustani</button>
      </div>
    </form>
    <p id="poruka" class="small"></p>

    <table class="table table-striped align-middle">
      <thead><tr><th>Naziv</th><th class="text-end">Cena</th><th class="text-end">Količina</th><th></th></tr></thead>
      <tbody id="tabela"></tbody>
    </table>
  </div>
  <script src="js/proizvodi.js"></script>
</body>
</html>

Sačuvaj kao wwwroot/index.html. Skripta je u posebnom fajlu wwwroot/js/proizvodi.js.

JavaScript: četiri operacije

// wwwroot/js/proizvodi.js – klijent za Web API (GET, POST, PUT, DELETE)
const API = "/api/proizvodi";            // ista aplikacija → relativna adresa, bez CORS-a
const $ = id => document.getElementById(id);

function poruka(tekst, greska = false) {
  $("poruka").textContent = tekst;
  $("poruka").className = "small " + (greska ? "text-danger" : "text-success");
}

// READ: učitaj i prikaži sve proizvode
async function ucitaj() {
  const odgovor = await fetch(API);
  const proizvodi = await odgovor.json();
  const tbody = $("tabela");
  tbody.replaceChildren();
  for (const p of proizvodi) {
    const red = document.createElement("tr");
    for (const [tekst, desno] of [[p.naziv, false], [p.cena + " din", true], [p.kolicina, true]]) {
      const td = document.createElement("td");
      td.textContent = tekst;                       // textContent – bezbedno
      if (desno) td.className = "text-end";
      red.append(td);
    }
    const akcije = document.createElement("td");
    akcije.className = "text-end text-nowrap";
    const izmeni = document.createElement("button");
    izmeni.className = "btn btn-sm btn-outline-primary me-1";
    izmeni.textContent = "Izmeni";
    izmeni.addEventListener("click", () => zaIzmenu(p));
    const obrisi = document.createElement("button");
    obrisi.className = "btn btn-sm btn-outline-danger";
    obrisi.textContent = "Obriši";
    obrisi.addEventListener("click", () => obrisiProizvod(p));
    akcije.append(izmeni, obrisi);
    red.append(akcije);
    tbody.append(red);
  }
}

function zaIzmenu(p) {                              // popuni formu podacima proizvoda
  $("id").value = p.id;
  $("naziv").value = p.naziv;
  $("cena").value = p.cena;
  $("kolicina").value = p.kolicina;
  $("sacuvaj").textContent = "Sačuvaj izmene";
  $("odustani").hidden = false;
}

function ocistiFormu() {
  $("forma").reset();
  $("id").value = "";
  $("sacuvaj").textContent = "Dodaj";
  $("odustani").hidden = true;
}

// CREATE ili UPDATE
$("forma").addEventListener("submit", async e => {
  e.preventDefault();
  const id = $("id").value;
  const podaci = { naziv: $("naziv").value.trim(), cena: Number($("cena").value), kolicina: Number($("kolicina").value) };
  const odgovor = await fetch(id ? `${API}/${id}` : API, {
    method: id ? "PUT" : "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(podaci)
  });
  if (odgovor.ok) {
    poruka(id ? "Proizvod je izmenjen." : "Proizvod je dodat.");
    ocistiFormu();
    ucitaj();
  } else {
    const greska = await odgovor.json().catch(() => null);     // 400: detalji provere sa servera
    const opis = greska && greska.errors ? Object.values(greska.errors).flat().join(" ") : odgovor.status;
    poruka("Greška: " + opis, true);
  }
});

$("odustani").addEventListener("click", ocistiFormu);

// DELETE
async function obrisiProizvod(p) {
  if (!confirm(`Obrisati „${p.naziv}“?`)) return;
  const odgovor = await fetch(`${API}/${p.id}`, { method: "DELETE" });
  poruka(odgovor.ok ? "Proizvod je obrisan." : "Brisanje nije uspelo.", !odgovor.ok);
  ucitaj();
}

ucitaj();
OperacijaZahtevŠta radi stranica posle
Readfetch(API)pravi redove tabele (textContent – bezbedno)
CreatePOST sa JSON telomporuka, prazna forma, ponovo učitava listu
UpdatePUT /api/proizvodi/{id}isto; forma je bila popunjena klikom na „Izmeni“
DeleteDELETE /api/proizvodi/{id}pita za potvrdu, pa ponovo učitava listu

Ako server odbije podatke (npr. negativna cena), odgovor 400 sadrži polje errors sa porukama iz atributa modela – stranica ih prikazuje. Proveru u pregledaču (required, min) korisnik može zaobići, ali proveru na serveru ne može.

Klijent na drugoj adresi: CORS

// Program.cs – dozvoli zahteve samo sa adrese klijenta
builder.Services.AddCors(opcije => opcije.AddPolicy("klijent", politika =>
    politika.WithOrigins("http://localhost:5500")      // adresa stranice klijenta
            .AllowAnyHeader()
            .AllowAnyMethod()));

var app = builder.Build();
app.UseCors("klijent");                                // pre MapControllers
app.MapControllers();

Pregledač ne dozvoljava stranici sa jedne adrese da čita odgovore sa druge, osim ako server to izričito dozvoli (zaglavlje Access-Control-Allow-Origin). Ne koristi AllowAnyOrigin() za API koji menja podatke. U klijentu tada adresa API-ja mora biti puna: const API = "http://localhost:5123/api/proizvodi".

Videi iz prve verzije lekcije

Napomena uz video: snimljen je za prvu verziju lekcija (.NET 5/6, klasa Startup, SQL Server i Dapper). Ideje su iste, ali je kod na ovoj strani pisan za današnji .NET (sve u Program.cs) – kada se razlikuju, važi kod sa strane.

Zadaci za vežbu

Za svaki zadatak postoji primer, uputstvo i rešenje.

ZADATAK 01Težina: srednje

Pretraga u klijentu

Dodaj iznad tabele polje „Pretraga“ koje pri svakom unosu poziva GET /api/proizvodi/pretraga?q=… (lekcija 4) i prikazuje samo pronađene proizvode; kada je polje prazno, prikazuju se svi.

Česte greške i saveti
  • Proveri adresu i port API-ja: greška Failed to fetch najčešće znači da server ne radi ili da je adresa pogrešna.
  • U konzoli pregledača poruka o CORS-u znači da klijent i API nisu na istoj adresi – dodaj CORS ili prebaci klijent u wwwroot.
  • Uz POST i PUT obavezno zaglavlje Content-Type: application/json, inače server vraća 415.

Proveri znanje

Otvori kviz

JavaScript klijent

1. Kojim zahtevom klijent menja postojeći proizvod?




2. Kada je potreban CORS?




3. Zašto se tekst iz baze upisuje sa textContent?