Kapitel 1: Introduktion till Webbutveckling

Välkommen till Glimåkras kurs i webbutveckling! I en värld där nästan allt finns online – från hur vi handlar och kommunicerar till hur vi lär oss och arbetar – har webben blivit en oumbärlig del av våra liv. Men vad är det egentligen som får webben att fungera, och hur skapas alla dessa webbplatser och applikationer vi använder dagligen?

Det är precis vad webbutveckling handlar om: konsten och tekniken att bygga för webben. Det är ett fält som kombinerar logiskt tänkande med kreativ design för att skapa digitala upplevelser.

Varför är detta viktigt för dig? Oavsett om du siktar på en karriär som utvecklare, vill bygga en egen webbplats, eller helt enkelt vill förstå den digitala världen bättre, ger kunskaper i webbutveckling dig värdefulla färdigheter.

I detta första kapitel lägger vi grunden. Vi kommer att utforska:

  • Vad webbutveckling faktiskt är: Vi bryter ner begreppet och tittar på de olika delarna som utgör en webbplats (frontend och backend) och hur de samspelar.
  • Webbens resa: En kort tillbakablick på hur webben har utvecklats från enkla textdokument till de komplexa applikationer vi ser idag, och en glimt av framtiden.
  • Utvecklarens verktygslåda: En översikt över de viktigaste språken, ramverken och verktygen som moderna webbutvecklare använder.
  • Komma igång: Vi ser till att du har de grundläggande verktygen installerade och redo att användas för resten av kursen.

Låt oss börja vår resa in i webbutvecklingens fascinerande värld!

Vad är webbutveckling?

Du använder webben varje dag – för att söka information, kommunicera, handla, eller titta på videor. Men har du någonsin stannat upp och funderat på hur allt detta fungerar? Hur skapas webbplatserna och applikationerna som gör detta möjligt?

Svaret är webbutveckling. Det är processen att designa, bygga och underhålla webbplatser och webbapplikationer. Det handlar om att kombinera teknisk skicklighet med kreativ problemlösning för att skapa de digitala upplevelser vi tar för givna.

Varför är det relevant för dig? Att förstå grunderna i webbutveckling ger dig inte bara insikt i hur en stor del av den moderna världen fungerar, utan öppnar också dörrar till att själv kunna skapa och forma digitalt innehåll och verktyg.

Två sidor av samma mynt: Frontend och Backend

Webbutveckling brukar delas upp i två huvudsakliga områden:

  1. Frontend (Klientsidan): Tänk på detta som allt du ser och interagerar med i din webbläsare. Det är layouten, färgerna, knapparna, formulären och animationerna. Frontend-utvecklare använder språk som:

    • HTML (HyperText Markup Language): Byggstenarna som strukturerar innehållet (rubriker, paragrafer, bilder, länkar).
    • CSS (Cascading Style Sheets): Reglerna som bestämmer hur innehållet ska se ut (färger, typsnitt, layout).
    • JavaScript: Språket som gör sidan interaktiv och dynamisk (hanterar klick, uppdaterar innehåll utan att ladda om sidan, skapar animationer).
  2. Backend (Serversidan): Detta är "bakom kulisserna". Backend hanterar logiken, databaserna och allt som sker på servern för att webbplatsen ska fungera. Tänk på det som motorn i en bil – du ser den inte direkt, men den är avgörande. Backend-utvecklare arbetar med:

    • Serverspråk: Språk som PHP (som vi fokuserar på i denna kurs), Python, Ruby, Node.js (JavaScript på servern), Java, m.fl. Dessa språk hanterar inkommande förfrågningar, bearbetar data och genererar svar.
    • Databaser: System som MySQL, PostgreSQL, MongoDB för att lagra och hämta information (t.ex. användardata, produkter, inlägg).
    • Servern: Datorn där webbplatsens kod körs och dess data lagras.

Analogi: Tänk på en restaurang. Frontend är matsalen – inredningen, menyn du läser, hur servitören interagerar med dig. Backend är köket – kocken som lagar maten (processar data), recepten (logiken), och skafferiet (databasen).

graph TD
    User["Användare"] -->|"Förfrågan (Request)"| Browser["Webbläsare/Klient"]
    Browser -->|"HTTP(S) Request"| Server["Webbserver/Backend"]
    Server -->|"Bearbetar & hämtar data"| Database[("Databas")]
    Database -->|"Data"| Server
    Server -->|"HTTP(S) Response (HTML, CSS, JS)"| Browser
    Browser -->|"Renderar sidan"| User

    subgraph Frontend ["Frontend"]
        Browser
    end

    subgraph Backend ["Backend"]
        Server
        Database
    end

Diagram: Förenklad bild av hur en webbförfrågan (request) hanteras.

Hur fungerar det? Request-Response Cycle

När du skriver in en webbadress (t.ex. www.example.com) eller klickar på en länk, händer följande i stora drag:

  1. Förfrågan (Request): Din webbläsare (klienten) skickar en förfrågan (HTTP request) över internet till den server där webbplatsen finns lagrad. Adressen översätts först från ett domännamn (som www.example.com) till en IP-adress via DNS (Domain Name System), ungefär som en telefonkatalog för internet.
  2. Bearbetning (Processing): Servern tar emot förfrågan. Om det är en enkel sida kan den direkt skicka tillbaka HTML-, CSS- och JavaScript-filer. Om det krävs logik (t.ex. logga in, hämta data) kör backend-koden, interagerar kanske med en databas.
  3. Svar (Response): Servern skickar tillbaka ett svar (HTTP response) till din webbläsare. Detta svar innehåller de filer och data som behövs för att visa sidan (oftast HTML, CSS, JavaScript).
  4. Rendering: Din webbläsare tar emot svaret, tolkar filerna och "ritar upp" webbsidan på din skärm.

Denna process, från förfrågan till svar, kallas Request-Response Cycle och är grunden för hur webben fungerar.

Fullstack-utvecklare

En person som behärskar både frontend- och backend-utveckling kallas ofta Fullstack-utvecklare. De har en bred förståelse för hela processen och kan bygga kompletta webbapplikationer på egen hand.

Viktiga Begrepp (Ordlista)

  • HTTP/HTTPS (HyperText Transfer Protocol/Secure): Protokollet (regelverket) för hur data skickas mellan klient och server på webben. HTTPS är den krypterade, säkra versionen.
  • URL (Uniform Resource Locator): Webbadressen som identifierar en specifik resurs (t.ex. en webbsida) på internet.
  • DNS (Domain Name System): Internets "telefonkatalog" som översätter läsbara domännamn till numeriska IP-adresser.
  • IP-adress (Internet Protocol Address): En unik numerisk adress för varje enhet ansluten till internet (t.ex. 192.168.1.1).
  • API (Application Programming Interface): Ett gränssnitt som låter olika program eller systemdelar kommunicera med varandra.

Sammanfattning

Webbutveckling är fältet där man skapar och underhåller webbplatser och applikationer. Det delas huvudsakligen upp i Frontend (det användaren ser och interagerar med, byggt med HTML, CSS, JavaScript) och Backend (logiken, databasen och servern som driver allt, ofta med språk som PHP). Förståelsen för hur klient och server kommunicerar via HTTP(S) i en Request-Response Cycle är central.

I nästa del tittar vi närmare på webbens historia och framtid.

Webbens Historia och Framtid

För att verkligen förstå webbutveckling idag är det värdefullt att känna till dess rötter och vart den är på väg. Webben, som vi känner den, är inte så gammal, men dess utveckling har varit explosionsartad.

Från Akademiskt Projekt till Global Plattform

  • Början (1989-1991): Allt startade vid CERN, det europeiska partikelfysiklaboratoriet. Forskaren Tim Berners-Lee ville skapa ett bättre sätt för forskare att dela information. Han uppfann de grundläggande teknologierna:

    • HTML (HyperText Markup Language): För att strukturera dokument.
    • HTTP (HyperText Transfer Protocol): För att överföra dokumenten.
    • URL (Uniform Resource Locator): För att ge varje dokument en unik adress. Han skapade också den första webbläsaren och webbservern. 1991 lanserades den allra första webbplatsen.
  • De Tidiga Åren (1990-talet): Webben växte långsamt först, främst inom akademiska kretsar. De första webbläsarna som Mosaic och senare Netscape Navigator gjorde webben tillgänglig för en bredare publik. Webbplatserna var mestadels statiska – enkla HTML-sidor med text och bilder, länkade till varandra. CSS introducerades för att separera utseende från struktur, och JavaScript började användas för enkel interaktivitet.

  • Web 2.0 och Dynamik (Sent 90-tal - 2000-tal): Behovet av mer interaktiva och dynamiska webbplatser växte. Detta ledde till utvecklingen av backend-språk (som PHP, ASP) och databaser. Nu kunde webbplatser:

    • Lagra användardata.
    • Skapa innehåll dynamiskt.
    • Möjliggöra användarinteraktion (kommentarer, forum, e-handel). Detta var eran för "Web 2.0", med fokus på sociala medier, bloggar och användargenererat innehåll.
  • Mobilens Era och Ramverk (2010-talet): Smarttelefonernas framväxt revolutionerade webben igen. Responsiv design blev avgörande – webbplatser måste anpassa sig till olika skärmstorlekar. Samtidigt utvecklades kraftfulla frontend-ramverk (som React, Angular, Vue.js) och backend-ramverk (som Laravel, Express) för att göra det enklare och snabbare att bygga komplexa webbapplikationer.

Nutid och Framtid

Webbutvecklingen står aldrig stilla. Några trender som formar webben idag och imorgon inkluderar:

  • Progressive Web Apps (PWA): Webbapplikationer som beter sig mer som native mobilappar (kan installeras, skicka notiser, fungera offline).
  • Single Page Applications (SPA): Webbapplikationer där innehållet uppdateras dynamiskt utan att hela sidan behöver laddas om, vilket ger en snabbare och smidigare användarupplevelse (ofta byggda med frontend-ramverk).
  • API-driven utveckling: Backend fokuserar på att skapa API:er som olika klienter (webb, mobilappar) kan konsumera.
  • WebAssembly (Wasm): Tillåter kod skriven i andra språk (som C++, Rust) att köras i webbläsaren med nära native prestanda, vilket öppnar för mer krävande applikationer som spel och videoredigering.
  • AI och Machine Learning: Integration av AI-funktioner direkt i webbapplikationer (chatbots, rekommendationssystem, bildanalys).
  • Fokus på prestanda och säkerhet: Ständig utveckling för att göra webben snabbare och säkrare.
  • Tillgänglighet (Accessibility): Ökad medvetenhet och krav på att webbplatser ska vara användbara för alla, oavsett funktionsvariationer.

Lär dig följa gällande standard

När webben var ung så var det inte ovanligt att vissa webbplatser fungerade endast om man använde speciella webbläsare. Som webbutvecklare är det viktigt att följa standarder som anger hur ny funktionalitet implementeras.

W3C är en organisation som publicerar gällande riktilinjer för webbens framtida utveckling. Det är bra att ibland läsa den dokumentation som finns tillgänglig för en viss webbstandard. Se mer här:
https://www.w3.org/TR/

W3C logotype

Sammanfattning

Webben har utvecklats från enkla, statiska textsidor till komplexa, dynamiska och interaktiva applikationer som är centrala i våra liv. Resan har gått via införandet av CSS och JavaScript, backend-teknologier, mobilanpassning och kraftfulla ramverk. Framtiden pekar mot ännu mer integrerade, intelligenta och högpresterande webbupplevelser.

Att förstå denna utveckling ger perspektiv på varför vi använder de verktyg och tekniker vi gör idag. Nästa avsnitt ger en översikt över den moderna webbutvecklarens verktygslåda.

Översikt över Webbutvecklarens Verktygslåda

Precis som en snickare behöver hammare och såg, behöver en webbutvecklare en uppsättning verktyg och tekniker för att bygga webbplatser och applikationer. Denna "verktygslåda" innehåller allt från grundläggande språk till avancerade ramverk och hjälpprogram.

Varför behöver vi dessa verktyg? Att bygga för webben involverar olika lager – struktur, utseende, interaktivitet, serverlogik, datalagring. Olika verktyg är specialiserade på olika delar av denna process.

Låt oss titta på de viktigaste kategorierna:

1. Grundläggande Byggstenar (Frontend)

Dessa är kärnan i allt som visas i en webbläsare:

  • HTML (HyperText Markup Language): Definierar strukturen och innehållet på en webbsida (rubriker, stycken, listor, bilder, länkar). Tänk på det som skelettet.
  • CSS (Cascading Style Sheets): Bestämmer presentationen och utseendet (färger, typsnitt, layout, positionering, responsivitet). Tänk på det som kläderna och sminket.
  • JavaScript: Lägger till interaktivitet och dynamiskt beteende (reagera på klick, validera formulär, hämta data, skapa animationer). Tänk på det som musklerna och nervsystemet.

2. Backend-Teknologier (Serversidan)

För att hantera logik, data och serverinteraktion:

  • Serverspråk: Språk som körs på webbservern för att bearbeta förfrågningar, interagera med databaser och generera dynamiskt innehåll. Vanliga exempel:
    • PHP: Mycket populärt, speciellt inom WordPress-världen (det vi fokuserar på).
    • Node.js: Låter dig köra JavaScript på servern.
    • Python: Används ofta med ramverk som Django och Flask.
    • Ruby: Känt för ramverket Ruby on Rails.
    • Java, C#: Vanliga i större företagsmiljöer.
  • Databaser: För att lagra och hantera data på ett strukturerat sätt.
    • SQL-databaser (Relationella): Lagrar data i tabeller med rader och kolumner (t.ex. MySQL, PostgreSQL). Vanligt för strukturerad data som användarprofiler, produkter.
    • NoSQL-databaser (Icke-relationella): Mer flexibla datamodeller (t.ex. MongoDB, Redis). Används ofta för ostrukturerad data, stora datamängder eller specifika behov som cachning.

3. Ramverk och Bibliotek

Dessa är förbyggda kodsamlingar som hjälper utvecklare att arbeta snabbare och mer strukturerat genom att erbjuda färdiga lösningar på vanliga problem:

  • Frontend-ramverk/bibliotek: För att bygga komplexa användargränssnitt (t.ex. React, Angular, Vue.js, Svelte).
  • Backend-ramverk: Ger struktur och verktyg för att bygga serverapplikationer (t.ex. Laravel (PHP), Express (Node.js), Django (Python), Ruby on Rails).
  • CSS-ramverk: Ger färdiga komponenter och stilar för snabbare design (t.ex. Bootstrap, Tailwind CSS).

4. Verktyg för Utvecklingsprocessen

Dessa hjälper till med kodning, samarbete och underhåll:

  • Texteditor/IDE (Integrated Development Environment): Program för att skriva och redigera kod (t.ex. Visual Studio Code (VS Code), Sublime Text, WebStorm). Erbjuder ofta funktioner som syntaxmarkering, kodkomplettering och felsökning.
  • Webbläsarens Utvecklarverktyg: Inbyggda verktyg i webbläsare (som Chrome DevTools, Firefox Developer Tools) för att inspektera HTML/CSS, felsöka JavaScript, analysera nätverkstrafik och prestanda. Oumbärliga!
  • Versionshanteringssystem: För att spåra ändringar i koden över tid och samarbeta med andra.
    • Git: Det absolut vanligaste systemet.
    • GitHub/GitLab/Bitbucket: Plattformar för att hosta Git-repositories (kodarkiv) online och underlätta samarbete.
  • Terminal/Kommandotolk: Ett textbaserat gränssnitt för att interagera med datorn, köra kommandon, installera verktyg och hantera filer. Väldigt viktigt för utvecklare (t.ex. Bash, Zsh på Linux/macOS, PowerShell eller WSL på Windows).
  • Pakethanterare: Verktyg för att installera och hantera externa bibliotek och beroenden (t.ex. npm/yarn för Node.js/JavaScript, Composer för PHP).

Sammanfattning

Webbutvecklarens verktygslåda är bred och innehåller allt från grundläggande språk (HTML, CSS, JS) och backend-teknologier (PHP, Node.js, databaser) till ramverk (React, Laravel) och viktiga hjälpprogram (VS Code, Git, Terminal). Att känna till dessa verktyg och förstå deras syfte är avgörande för att kunna bygga moderna webbapplikationer.

I nästa avsnitt går vi igenom hur du installerar några av de mest grundläggande verktygen vi kommer använda i denna kurs.

Installera och Konfigurera Verktyg

Nu när vi har en översikt över vad webbutveckling är och vilka verktyg som används, är det dags att sätta upp vår egen grundläggande utvecklingsmiljö. Vi behöver några centrala verktyg för att kunna skriva kod, spara dess historik och köra kommandon.

Mål: I detta avsnitt ser vi till att du har följande installerat och redo att användas:

  1. Visual Studio Code (VS Code): En modern och kraftfull texteditor.
  2. Git: För versionshantering av vår kod.
  3. En Terminal: För att interagera med datorn via kommandon.

1. Visual Studio Code (VS Code)

VS Code är en gratis, populär och mycket utbyggbar kod-editor från Microsoft. Den fungerar på Windows, macOS och Linux.

  • Varför VS Code? Den har inbyggt stöd för många språk (inklusive HTML, CSS, JavaScript, PHP), bra integration med Git, en stor marknadsplats för tillägg (extensions) och är relativt resurssnål.

  • Installation:

    1. Gå till den officiella webbplatsen: https://code.visualstudio.com/
    2. Ladda ner installationsprogrammet för ditt operativsystem (Windows, macOS, Linux).
    3. Kör installationsprogrammet och följ instruktionerna. Acceptera standardinställningarna om du är osäker.
  • Grundläggande konfiguration:

    • Svenskt språkpaket (ej rekommenderat): Engleska är det universala språket inom programmering. Majoriteten av arbetsplatser använder språket för all kommunikation. Men om du bara vill komma igång med programmering och Svenska fungerar mycket bättre. Öppna VS Code. Tryck på Ctrl+Shift+P (eller Cmd+Shift+P på Mac) för att öppna kommandopaletten. Skriv Configure Display Language, välj det och sedan Install additional languages.... Sök efter Swedish Language Pack for Visual Studio Code och installera det. Starta om VS Code när du uppmanas.
    • Utforska tillägg (Extensions): Klicka på ikonen för tillägg i sidopanelen (ser ut som fyra rutor, en separerad). Här kan du söka efter och installera tillägg som kan underlätta ditt arbete. Några populära för webbutveckling är:
      • Prettier - Code formatter: Formaterar din kod automatiskt.
      • Live Server: Startar en lokal utvecklingsserver med live-reload för statiska sidor.
      • PHP Intelephense: Ger bättre kodkomplettering och analys för PHP. (Kan göras senare)

2. Git

Git är ett distribuerat versionshanteringssystem. Det låter dig spara ögonblicksbilder (commits) av din kod, gå tillbaka till tidigare versioner, skapa olika utvecklingsgrenar (branches) och samarbeta med andra.

  • Varför Git? Det är industristandard och en fundamental färdighet för alla utvecklare. Det hjälper dig att:

    • Hålla ordning på ändringar.
    • Undvika att förlora arbete.
    • Experimentera säkert med ny kod.
    • Samarbeta på projekt (ofta via plattformar som GitHub).
  • Installation:

    • Windows: Ladda ner och installera Git for Windows från https://git-scm.com/download/win. Under installationen, välj de rekommenderade inställningarna om du är osäker. Se till att Git kan användas från kommandotolken (ofta standardvalet). Git Bash kommer också installeras, vilket ger dig en bra terminalmiljö.
    • macOS: Git är ofta förinstallerat. Öppna Terminalen (Program > Verktygsprogram > Terminal) och skriv git --version. Om du får ett versionsnummer är det installerat. Annars kan du installera det via Xcode Command Line Tools (kör xcode-select --install i Terminalen) eller ladda ner det från https://git-scm.com/download/mac.
    • Linux (Debian/Ubuntu): Öppna terminalen och kör: sudo apt update && sudo apt install git
    • Linux (Fedora): Öppna terminalen och kör: sudo dnf install git
  • Grundläggande konfiguration (Gör detta efter installation!): Öppna din terminal (Terminal på Mac/Linux, Git Bash eller Kommandotolken/PowerShell på Windows) och kör följande kommandon, men byt ut namnet och e-postadressen mot dina egna. Dessa används för att identifiera vem som gjort ändringarna i Git.

    git config --global user.name "Ditt Namn"
    git config --global user.email "din.epost@example.com"
    

3. Terminalen / Kommandotolken

Terminalen (även kallad kommandotolk, shell, command line) är ett textbaserat gränssnitt där du kan skriva kommandon för att interagera med ditt operativsystem och olika program (som Git).

  • Varför Terminalen? Många utvecklingsverktyg och processer (som att köra Git-kommandon, installera paket, starta servrar) hanteras effektivt via terminalen. Det är ett kraftfullt verktyg att bli bekväm med.

  • Vilken ska jag använda?

    • macOS: Använd den inbyggda Terminal-appen (finns i /Applications/Utilities/ eller /Program/Verktygsprogram/).
    • Linux: Använd den terminalemulator som följer med din distribution (t.ex. GNOME Terminal, Konsole).
    • Windows:
      • Git Bash: Installeras med Git for Windows och ger en Unix-liknande miljö, vilket ofta är att föredra för webbutveckling.
      • Windows Terminal: En modern terminalapp från Microsoft som kan köra PowerShell, Kommandotolken (cmd) och WSL. Rekommenderas om du vill ha en mer modern Windows-upplevelse. Kan installeras från Microsoft Store.
      • WSL (Windows Subsystem for Linux): Låter dig köra en riktig Linux-miljö direkt på Windows. Lite mer avancerat att sätta upp, men väldigt kraftfullt för webbutveckling. (Se Microsofts WSL-dokumentation för installationsguide).
  • Testa: Öppna din valda terminal och prova att skriva git --version. Om du ser ett versionsnummer fungerar både terminalen och Git-installationen!

Sammanfattning

Du bör nu ha installerat och grundkonfigurerat Visual Studio Code för att skriva kod, Git för att hantera kodens historik, och ha tillgång till en Terminal för att köra kommandon. Dessa tre verktyg utgör grunden i vår utvecklingsmiljö för denna kurs.

I nästa kapitel börjar vi använda dessa verktyg när vi dyker ner i HTML och lär oss grunderna i Git.

Kapitel 2: HTML och Git – Webbens byggstenar och kodens historik

I det föregående kapitlet fick vi en överblick över webbutvecklingens värld. Nu är det dags att kavla upp ärmarna och börja bygga!

Detta kapitel introducerar två fundamentala teknologier som alla webbutvecklare måste behärska:

  1. HTML (HyperText Markup Language): Språket vi använder för att definiera strukturen och innehållet på en webbsida. Tänk på det som skelettet som håller allt på plats. Utan HTML finns det ingen webbsida.
  2. Git: Systemet vi använder för att spara och hantera olika versioner av vår kod. Det är som en tidsmaskin och ett samarbetsverktyg i ett – helt avgörande när projekt växer eller när flera personer arbetar tillsammans.

Varför dessa två tillsammans?
När du börjar skriva HTML-kod, även för enkla sidor, vill du kunna spara ditt arbete på ett säkert sätt, kunna gå tillbaka om något går fel och ha en metod för att dela din kod. Git ger dig den tryggheten och strukturen redan från början.


Vad kommer du att lära dig i detta kapitel?

  • Grunderna i HTML5: De vanligaste taggarna för att skapa text, rubriker, listor, länkar och bilder.
  • Struktur och semantik: Varför det är viktigt att använda rätt HTML-element för rätt syfte – inte bara för utseendet, utan också för sökmotorer och tillgänglighet.
  • Tillgänglighet (accessibility): Hur HTML kan användas för att göra webbplatser användbara för fler människor, inklusive de med funktionsvariationer.
  • Introduktion till Git: Vad versionshantering är och varför det är så viktigt.
  • Grundläggande Git-kommandon: Hur du praktiskt kommer igång med att skapa ett repository, spara ändringar (commits) och se historik.
  • Arbeta med GitHub: Hur du använder en plattform som GitHub för att lagra din kod online och (senare) samarbeta med andra.
  • Praktiska övningar: Du får bygga enkla HTML-strukturer och hantera dem med Git.

Låt oss börja med att lägga grunden för våra webbsidor med HTML!

Grunderna i HTML5: Att strukturera webbens innehåll

HTML (HyperText Markup Language) är skelettet i varje webbsida. Det är inte ett programmeringsspråk, utan ett märkspråk (markup language). Vi använder taggar (tags) för att berätta för webbläsaren hur innehållet ska struktureras och vad de olika delarna betyder.

Mål:
Förstå grundstrukturen i ett HTML-dokument och lära dig de vanligaste taggarna för att skapa text, rubriker, listor, länkar och bilder.


HTML-dokumentets grundstruktur

Varje HTML-sida följer en grundläggande mall. Här är ett minimalt exempel:

<!DOCTYPE html>
<html lang="sv">
<head>
    <meta charset="UTF-8">
    <title>Min första webbsida</title>
</head>
<body>
    <h1>Hej världen!</h1>
    <p>Detta är min första webbsida.</p>
</body>
</html>

Förklaring av delarna:

  • <!DOCTYPE html>: Deklaration som talar om för webbläsaren att dokumentet är HTML5. Ska alltid vara först.
  • <html>: Rotelementet (root element) som omsluter allt innehåll på sidan (förutom DOCTYPE). Attributet lang="sv" anger att sidans språk är svenska.
  • <head>: Innehåller metadata om dokumentet – information som inte visas direkt på sidan men är viktig för webbläsare och sökmotorer.
    • <meta charset="UTF-8">: Anger teckenkodningen. UTF-8 stöder de flesta tecken (inklusive å, ä, ö).
    • <title>: Sidans titel, visas i webbläsarens flik och används av sökmotorer.
  • <body>: Innehåller allt synligt innehåll på webbsidan – text, rubriker, bilder, länkar, listor, etc.

Taggar, element och attribut

  • Taggar (tags): Markerar början och slutet på ett element. Skrivs inom vinkelparenteser (< >). De flesta taggar kommer i par: en starttagg (t.ex. <p>) och en sluttagg (t.ex. </p>).
  • Element: Ett komplett HTML-element består av en starttagg, innehåll och en sluttagg. Exempel: <p>Detta är en paragraf.</p>.
    • Tomma element (empty elements): Har ingen sluttagg, t.ex. <br>, <img>, <meta>.
  • Attribut (attributes): Ger extra information om ett element. Skrivs i starttaggen och består av namn och värde (inom citationstecken). Exempel: <img src="bild.jpg" alt="Beskrivning">.
    • lang: Anger språk.
    • src: (source) Bildens källa.
    • alt: (alternative text) Alternativ text för bilder (viktigt för tillgänglighet).
    • href: (hypertext reference) Länkens mål.

Vanliga HTML-element

Rubriker (<h1> till <h6>)

Används för att definiera rubriknivåer. <h1> är viktigast (sidans huvudrubrik), <h6> minst viktig.

<h1>Huvudrubrik</h1>
<h2>Underrubrik</h2>
<h3>Under-underrubrik</h3>

Paragrafer (stycken) (<p>)

Används för att gruppera text i stycken.

<p>Detta är det första stycket med text.</p>
<p>Detta är ett andra stycke.</p>

Länkar (anchors) (<a>)

Skapar hyperlänkar till andra sidor eller resurser. href är obligatoriskt och anger destinationen.

<a href="https://www.google.com">Sök på Google</a>
<a href="annan_sida.html">Länk till en lokal sida</a>

Bilder (<img>)

Bäddar in bilder. Tomt element (ingen sluttagg). src anger bildens källa, alt är alternativ text.

<img src="bilder/logo.png" alt="Företagets logotyp">

Listor

  • Oordnade listor (unordered lists): <ul> (punkter)
  • Ordnade listor (ordered lists): <ol> (nummer)
  • Listelement (list items): <li> (används i <ul> eller <ol>)
<ul>
    <li>Äpple</li>
    <li>Banan</li>
    <li>Päron</li>
</ul>

<ol>
    <li>Steg 1: Gör något</li>
    <li>Steg 2: Gör något annat</li>
    <li>Steg 3: Klart!</li>
</ol>

Radbrytning (<br>)

Skapar en enkel radbrytning. Använd sparsamt – strukturera hellre med paragrafer.

Kommentarer (<!-- ... -->)

Kommentarer syns inte i webbläsaren men är användbara för anteckningar i koden.

<!-- Detta är en kommentar -->

HTML – en standard

HTML utvecklas ständigt. Använd alltid den aktuella standarden:
https://html.spec.whatwg.org/multipage/


Sammanfattning

  • HTML är grunden för allt webbinnehåll.
  • Ett HTML-dokument består av <!DOCTYPE>, <html>, <head>, <body>.
  • Taggar, element och attribut används för att strukturera och beskriva innehållet.
  • Vanliga element: rubriker (<h1><h6>), paragrafer (<p>), länkar (<a>), bilder (<img>), listor (<ul>, <ol>, <li>).
  • Skriv ren och korrekt HTML – det är första steget mot fungerande webbsidor.

I nästa avsnitt lär du dig hur semantisk HTML kan ge ditt innehåll ännu mer mening och struktur.

Strukturera och ge semantisk betydelse åt webbinnehåll med HTML5

Vi har lärt oss de grundläggande HTML-taggarna för att skapa innehåll. Men hur organiserar vi innehållet på ett logiskt sätt? Och hur kan vi ge innehållet mening utöver bara dess utseende? Det är här struktur och semantik kommer in i bilden.

  • Struktur: Handlar om hur vi organiserar och grupperar innehållet på sidan med hjälp av olika HTML-element.
  • Semantik: Handlar om att använda HTML-element som korrekt beskriver innebörden eller syftet med innehållet de omsluter.

Varför är detta viktigt?

  1. Tillgänglighet (accessibility): Skärmläsare och andra hjälpmedel använder HTML-strukturen och semantiken för att förstå och navigera på sidan. Korrekt semantik gör webbplatsen användbar för fler människor.
  2. Sökmotoroptimering (SEO): Sökmotorer som Google analyserar HTML-strukturen för att förstå vad sidan handlar om och rangordna den i sökresultaten. Semantisk HTML hjälper dem att göra ett bättre jobb.
  3. Underhållbarhet: En välstrukturerad och semantisk kodbas är lättare att läsa, förstå och underhålla för dig själv och andra utvecklare.
  4. Styling med CSS: Även om CSS styr utseendet, ger en bra HTML-struktur tydliga "krokar" som CSS kan fästa vid för att applicera stilar.

Allmänna strukturelement: <div> och <span>

Historiskt (och fortfarande idag) används ofta två allmänna (generiska) element för att gruppera innehåll:

  • <div> (division): Ett blockelement (block-level element). Används för att gruppera större sektioner av innehåll eller för layoutändamål. Börjar på en ny rad och tar upp hela tillgängliga bredden.
  • <span>: Ett inline-element. Används för att gruppera mindre delar av text eller annat inline-innehåll, ofta för att applicera specifik styling eller för att identifiera en textdel med JavaScript. Börjar inte på en ny rad.

Problem: <div> och <span> är icke-semantiska. De säger ingenting om vad innehållet de omsluter faktiskt representerar.

<!-- Icke-semantiskt exempel -->
<div id="header">...</div>
<div id="nav">...</div>
<div class="article">
  <div class="headline">Artikelrubrik</div>
  <p>...</p>
</div>
<div id="footer">...</div>

Detta fungerar, men det kräver att man inspekterar id eller class-attribut för att gissa sig till syftet.


HTML5 semantiska strukturelement

HTML5 introducerade nya element specifikt för att ge semantisk betydelse åt olika delar av en webbsidas struktur. Dessa gör koden tydligare och mer meningsfull.

graph TD
    subgraph Sida
    Header[<header>]
    Nav[<nav>]
    Main[<main>]
    Aside[<aside>]
    Footer[<footer>]

    Header --> Nav
    Header --> Main
    Nav --> Main
    Main --- Aside
    Main --> Footer
    Aside --> Footer
    end

    subgraph MainContent
        direction LR
        Section1[<section>]
        Article1[<article>]
        Section2[<section>]

        Main --> Section1
        Main --> Article1
        Main --> Section2
    end

Diagram: Vanlig sidlayout med HTML5 semantiska element.

Här är de viktigaste:

  • <header>: Representerar introducerande innehåll för en sida eller en sektion. Innehåller ofta sidans logotyp, huvudrubrik (<h1>), och kanske primär navigering.
  • <nav>: Representerar en sektion med navigeringslänkar (t.ex. huvudmenyn, länkar inom sidan).
  • <main>: Representerar huvudinnehållet i dokumentet. Det ska bara finnas ett <main>-element per sida, och det ska inte placeras inuti <article>, <aside>, <header>, <nav>, eller <footer>.
  • <article>: Representerar en fristående, komplett innehållsdel som skulle kunna distribueras oberoende av resten av sidan (t.ex. ett blogginlägg, en forumkommentar, en nyhetsartikel).
  • <section>: Representerar en tematisk gruppering av innehåll, vanligtvis med en egen rubrik (<h2><h6>). Används när det inte finns något mer specifikt semantiskt element (som <article> eller <nav>). Tänk på det som ett kapitel i en bok.
  • <aside>: Representerar innehåll som är tangentiellt relaterat till innehållet runt omkring det (t.ex. en sidopanel med relaterade länkar, författarinformation, reklam). Kan ses som en fotnot eller en sidnotering.
  • <footer>: Representerar sidfoten för en sida eller sektion. Innehåller ofta copyrightinformation, länkar till sekretesspolicy, kontaktuppgifter.

Exempel: Strukturera och ge semantisk betydelse

<header>
  <h1>Webbplatsens Titel</h1>
  <nav>
    <ul>
      <li><a href="/">Hem</a></li>
      <li><a href="/om">Om oss</a></li>
    </ul>
  </nav>
</header>

<main>
  <article>
    <h2>Artikelrubrik</h2>
    <p>...</p>
    <section>
      <h3>Kommentarer</h3>
      <p>...</p>
    </section>
  </article>

  <aside>
    <h3>Relaterade länkar</h3>
    <ul>...</ul>
  </aside>
</main>

<footer>
  <p>&copy; 2024 Webbplats AB</p>
</footer>

Denna version är mycket tydligare. Bara genom att titta på taggarna förstår vi syftet med de olika sektionerna.


När ska man använda <div>?

Även med de semantiska elementen finns det fortfarande tillfällen då <div> är lämpligt:

  • Endast för styling/layout: Om du behöver gruppera element enbart för att applicera CSS-regler (t.ex. skapa en container för att centrera innehåll) och det inte finns något semantiskt element som passar, är <div> rätt val.
  • JavaScript-krokar: Om du behöver ett element att fästa JavaScript-funktionalitet vid och ingen semantisk tagg passar.

Försök dock alltid att först använda ett semantiskt element om det finns ett som beskriver innehållets syfte.


Sammanfattning

Att använda semantisk HTML handlar om att välja de HTML-element som bäst beskriver innebörden av ditt innehåll. HTML5 erbjuder specifika element som <header>, <nav>, <main>, <article>, <section>, <aside>, och <footer> för att strukturera sidans huvuddelar på ett meningsfullt sätt. Detta förbättrar tillgänglighet, SEO och kodens läsbarhet jämfört med att bara använda generiska <div>-element.

I nästa avsnitt fokuserar vi specifikt på hur HTML-element bidrar till att göra webbplatser mer

HTML och tillgänglighet (accessibility)

Vi har pratat om semantisk HTML och hur det hjälper maskiner (som sökmotorer och skärmläsare) att förstå vårt innehåll. En av de absolut viktigaste anledningarna till att använda korrekt, semantisk HTML är webbtillgänglighet (ofta förkortat a11y – 'a' följt av 11 bokstäver och ett 'y').

Vad är webbtillgänglighet?
Det handlar om att designa och utveckla webbplatser och applikationer så att de kan användas av alla människor, oavsett deras förmågor eller funktionsvariationer. Detta inkluderar personer med:

  • Synnedsättningar (blinda, svagsynta, färgblinda)
  • Hörselnedsättningar
  • Motoriska funktionsnedsättningar (svårt att använda mus eller tangentbord)
  • Kognitiva funktionsnedsättningar (inlärningssvårigheter, minnesproblem)

Varför är det viktigt?

  1. Mänskliga rättigheter: Alla har rätt att ta del av information och tjänster online.
  2. Lagar och regler: Många länder (inklusive inom EU) har lagkrav på att offentliga och vissa privata webbplatser ska vara tillgängliga (t.ex. Webbtillgänglighetsdirektivet).
  3. Bättre användarupplevelse för alla: Principer för god tillgänglighet leder ofta till en bättre och tydligare upplevelse för samtliga användare.
  4. SEO: God tillgänglighet och god SEO går ofta hand i hand, eftersom båda gynnas av tydlig struktur och semantik.

Hur HTML bidrar till tillgänglighet

Att använda HTML korrekt är grunden för en tillgänglig webbplats. Här är några nyckelområden där HTML spelar en avgörande roll:

1. Semantisk struktur

  • Använd semantiska element som <header>, <nav>, <main>, <article>, <section>, <aside>, <footer> för att definiera sidans regioner. Detta gör det möjligt för skärmläsaranvändare att snabbt hoppa mellan olika delar av sidan.
  • Använd rubriktaggar (<h1><h6>) i korrekt hierarkisk ordning för att skapa en logisk disposition.
  • Använd listor (<ul>, <ol>, <li>) för att gruppera relaterade objekt.

2. Alternativtext för bilder (alt-attributet)

  • ALLTID inkludera ett meningsfullt alt-attribut på <img>-taggar.
  • Beskriv syftet eller innehållet i bilden kortfattat.
  • Om bilden är dekorativ (inte förmedlar information), använd ett tomt alt-attribut: alt="". Då ignoreras bilden av skärmläsare.
<!-- Bra alt-text -->
<img src="hund.jpg" alt="En glad golden retriever som leker i parken">

<!-- Dekorativ bild -->
<img src="gra_linje.png" alt="">

3. Beskrivande länktexter

  • Undvik vaga länktexter som "Klicka här" eller "Läs mer".
  • Länktexten ska tydligt beskriva vart länken leder, även när den läses utanför sitt sammanhang.
<!-- Undvik -->
<p>För mer information, <a href="rapport.pdf">klicka här</a>.</p>

<!-- Föredra -->
<p>Läs hela <a href="rapport.pdf">årsrapporten för 2023 (PDF)</a>.</p>

4. Formuläretiketter (<label>)

  • Alla formulärkontroller (<input>, <textarea>, <select>) måste ha en associerad <label>.
  • Labeln beskriver vad fältet är till för.
  • Använd for-attributet på <label> för att koppla det till id-attributet på formulärkontrollen. Detta gör att skärmläsare kan läsa upp etiketten när fältet fokuseras, och användare kan klicka på etiketten för att aktivera fältet.
<label for="user_email">E-postadress:</label>
<input type="email" id="user_email" name="email">

5. Språkattribut (lang)

  • Ange sidans huvudsakliga språk på <html>-taggen (<html lang="sv">).
  • Om en del av innehållet är på ett annat språk, ange det med lang-attributet på det omslutande elementet. Detta hjälper skärmläsare att använda korrekt uttal.
<p>Han sa <span lang="en">"Hello world"</span>.</p>

6. Tabeller för data

  • Använd tabeller (<table>) endast för att presentera tabulär data, inte för layout.
  • Använd <thead> för tabellrubriker, <tbody> för tabelldata, och <th> (table header) för rubrikceller. Använd scope-attributet (scope="col" för kolumnrubriker, scope="row" för radrubriker) för att tydligt koppla dataceller till sina rubriker.
<table>
  <thead>
    <tr>
      <th scope="col">Produkt</th>
      <th scope="col">Pris</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Äpple</td>
      <td>5 kr</td>
    </tr>
  </tbody>
</table>

Kort om WAI-ARIA

För mer komplexa komponenter och interaktioner (som ofta skapas med JavaScript) räcker ibland inte vanlig HTML till för att förmedla all nödvändig information till hjälpmedel. Då kan WAI-ARIA (Web Accessibility Initiative – Accessible Rich Internet Applications) användas. ARIA är en uppsättning speciella attribut som kan läggas till i HTML för att förbättra tillgängligheten, t.ex. för att beskriva roller (role="button"), tillstånd (aria-pressed="true") och egenskaper hos dynamiska komponenter.

Vi går inte djupt in på ARIA i denna grundkurs, men det är bra att känna till att det finns för mer avancerade behov.


Sammanfattning

Webbtillgänglighet handlar om att skapa webbplatser som fungerar för alla. Korrekt och semantisk HTML är grunden för detta. Genom att använda semantiska strukturelement, meningsfulla alt-texter för bilder, beskrivande länktexter, korrekta formuläretiketter (<label>), och ange språk (lang), skapar vi en mer robust och tillgänglig upplevelse. Att tänka på tillgänglighet från början är inte bara en god praxis, utan ofta också ett lagkrav och gynnar alla användare.

Nu när vi har en god grund i HTML är det dags att titta på det andra viktiga verktyget i detta kapitel: Git.

Introduktion till Git: Varför Versionshantering?

Föreställ dig att du arbetar på ett viktigt dokument. Du gör ändringar, sparar, gör fler ändringar... men så inser du att en ändring du gjorde för två timmar sedan var fel. Hur hittar du tillbaka till den versionen? Eller tänk dig att du vill prova en ny idé, men är rädd för att förstöra det du redan har. Eller ännu värre, tänk om flera personer behöver arbeta på samma dokument samtidigt?

Det är här versionshantering (version control) kommer in. Ett versionshanteringssystem (Version Control System, VCS) är ett verktyg som hjälper dig att spåra och hantera ändringar i filer över tid.

Git är det absolut mest populära och dominerande distribuerade versionshanteringssystemet idag. Det utvecklades ursprungligen av Linus Torvalds (skaparen av Linux) för att hantera utvecklingen av Linux-kärnan.

Motivation:
Git gör det möjligt att experimentera, samarbeta och alltid kunna återställa eller förstå vad som hänt i projektets historik. Det är en grundläggande färdighet för alla som arbetar med kod.


Varför är Git så viktigt för utvecklare?

  1. Historik och återställning:
    Git sparar ögonblicksbilder (kallade commits) av ditt projekt. Du kan se exakt vem som ändrade vad och när, och du kan enkelt återgå till vilken tidigare version som helst. Det är som en obegränsad "ångra"-knapp för hela ditt projekt.

  2. Förgrening (branching):
    Git gör det otroligt enkelt att skapa separata "grenar" (branches) av ditt projekt. Du kan arbeta på en ny funktion eller experimentera på en egen gren utan att påverka huvudversionen (ofta kallad main). När du är klar kan du sammanfoga (merge) dina ändringar tillbaka till huvudgrenen.

  3. Samarbete:
    Git är designat för distribuerat arbete. Varje utvecklare har en komplett kopia av projektets historik. Plattformar som GitHub, GitLab och Bitbucket bygger på Git och gör det enkelt att dela kod, granska varandras ändringar (Pull Requests) och arbeta tillsammans i team.

  4. Trygghet:
    Genom att regelbundet spara dina ändringar (committa) och eventuellt skicka dem till en fjärrserver (som GitHub), minskar du risken att förlora arbete på grund av hårddiskkrascher eller misstag.

  5. Industristandard:
    Kunskap i Git är i princip ett krav för de flesta utvecklarjobb idag.


Grundläggande koncept i Git

  • Repository (repo): En "behållare" eller mapp som innehåller alla filer för ditt projekt samt hela dess ändringshistorik (i en dold mapp kallad .git).
  • Commit: En sparad ögonblicksbild av ditt projekts filer vid en viss tidpunkt. Varje commit har ett unikt ID och ett meddelande som beskriver ändringarna.
  • Branch: En oberoende utvecklingslinje. Standardgrenen heter ofta main.
  • Checkout: Processen att byta mellan olika branches eller återställa filer till en specifik commit.
  • Merge: Processen att kombinera ändringar från en branch till en annan.
  • Clone: Att skapa en lokal kopia av ett befintligt repository (ofta från en fjärrserver som GitHub).
  • Push: Att skicka dina lokala commits till ett fjärr-repository (t.ex. på GitHub).
  • Pull: Att hämta ändringar från ett fjärr-repository och integrera dem i din lokala branch.
  • Working directory: De filer du ser i din projektmapp.
  • Staging area (index): Ett mellanläge där du förbereder vilka ändringar som ska inkluderas i nästa commit.

Git-arbetsflöde: Så här hänger det ihop

graph LR
    subgraph "Lokalt Repository"
        WD["Working Directory"] -- "git add" --> SA["Staging Area"];
        SA -- "git commit" --> LH["Local History/.git"];
    end

    subgraph "Fjärr-Repository (t.ex. GitHub)"
      RH["Remote History"]
    end

    LH -- "git push" --> RH;
    RH -- "git pull/fetch" --> LH;
    LH -- "git checkout" --> WD;

Diagram: Förenklat arbetsflöde i Git.

  • Working Directory: Dina faktiska filer på datorn.
  • Staging Area: Här samlar du ihop de ändringar du vill spara i nästa commit.
  • Local History (.git): Här sparas alla commits och projektets historik.
  • Remote History: Fjärrkopian av projektet, t.ex. på GitHub.

Sammanfattning

Git är ett kraftfullt versionshanteringssystem som är oumbärligt för modern mjukvaruutveckling. Det hjälper oss att spåra ändringar, samarbeta effektivt, experimentera säkert och undvika dataförlust. Att förstå grundläggande koncept som repository, commit, branch och staging area är nyckeln till att kunna använda Git effektivt.

I nästa avsnitt går vi igenom de vanligaste Git-kommandona du behöver för att komma igång

Grundläggande Git-kommandon: Det dagliga arbetsflödet

Nu när vi förstår varför Git är användbart och känner till de grundläggande koncepten, är det dags att se hur vi använder det i praktiken. Detta görs via kommandon som vi skriver i terminalen (som vi installerade i kapitel 1).

Mål:
Lära oss och förstå de vanligaste Git-kommandona för att skapa ett repository, spåra filer, spara ändringar (commits) och se status och historik.

Förutsättning:
Du har Git installerat och konfigurerat (med ditt namn och e-post, se kapitel 1).

Viktigt:
Alla Git-kommandon börjar med git. Kommandona körs i terminalen inuti din projektmapp.


1. Skapa ett repository: git init

Det allra första steget för ett nytt projekt är att initiera ett Git-repository.

  • Kommando:
    git init
  • Vad det gör:
    Skapar en ny, dold undermapp .git i din nuvarande mapp. Denna .git-mapp innehåller all information och historik för ditt repository. Detta behöver bara göras en gång per projekt.

Så här gör du:

  1. Skapa en ny mapp för ditt projekt (t.ex. mitt-projekt).
  2. Navigera in i mappen i din terminal (cd mitt-projekt).
  3. Kör kommandot:
    git init
    
    Du bör se ett meddelande som liknar:
    Initialized empty Git repository in /path/to/your/mitt-projekt/.git/

2. Kontrollera status: git status

Detta är ett av de viktigaste och mest använda kommandona. Det visar det aktuella tillståndet för ditt repository.

  • Kommando:
    git status
  • Vad det gör:
    Talar om:
    • Vilken branch du är på.
    • Om det finns ändringar i Working Directory som inte är i Staging Area.
    • Om det finns filer i Staging Area som väntar på att committas.
    • Om det finns filer som Git inte spårar alls (untracked files).

Så här gör du:

  1. Skapa en ny fil i din projektmapp, t.ex. index.html.
  2. Kör git status i terminalen. Du kommer se att index.html listas under "Untracked files", eftersom Git ser filen men inte spårar ändringar i den än.

3. Lägga till filer i Staging Area: git add

Innan du kan spara en ändring (committa) måste du tala om för Git vilka ändringar som ska inkluderas. Detta görs genom att lägga till dem i Staging Area.

  • Kommandon:
    • git add <filnamn>: Lägger till en specifik fil (t.ex. git add index.html).
    • git add . : Lägger till alla nya och ändrade filer i den nuvarande mappen och dess undermappar till Staging Area. (Punkten . representerar nuvarande mapp). Används ofta.
  • Vad det gör:
    Flyttar ändringarna från Working Directory till Staging Area, redo för nästa commit.

Så här gör du:

  1. Se till att du har skapat index.html.
  2. Kör git add index.html (eller git add .).
  3. Kör git status igen. Nu bör du se index.html listad under "Changes to be committed".

4. Spara ändringar: git commit

När du har lagt till de ändringar du vill spara i Staging Area, skapar du en commit.

  • Kommando:
    git commit -m "Ditt commit-meddelande"
  • Vad det gör:
    Tar en ögonblicksbild av alla filer som finns i Staging Area och sparar den permanent i .git-mappens historik. Varje commit måste ha ett commit-meddelande (efter -m) som kortfattat beskriver vad ändringen handlar om (t.ex. "Skapa grundläggande HTML-struktur", "Lägg till bildlänk").
    • Bra commit-meddelanden är viktiga! De ska vara korta, beskrivande och skrivna i imperativ (t.ex. "Lägg till bild..." istället för "Lade till bilden...").

Så här gör du:

  1. Se till att du har kört git add.
  2. Kör
    git commit -m "Skapa tom index.html-fil"
    
  3. Kör git status igen. Nu bör det stå något i stil med "nothing to commit, working tree clean", vilket betyder att alla spårade ändringar är sparade.

5. Visa historik: git log

För att se listan över alla commits som gjorts i projektet.

  • Kommando:
    git log
  • Vad det gör:
    Visar historiken med den senaste committen överst. För varje commit visas dess unika ID (en lång hash-sträng), författaren, datumet och commit-meddelandet.
    • Tryck q för att avsluta loggvisningen om den är lång.
    • git log --oneline: Visar en mer kompakt vy med bara commit-ID och meddelande.

Så här gör du:

  1. Gör en ändring i index.html (lägg till lite text).
  2. Kör git add .
  3. Kör
    git commit -m "Lägg till innehåll i index.html"
    
  4. Kör git log (eller git log --oneline). Du bör nu se dina två commits i historiken.

Sammanfattning: Det grundläggande arbetsflödet

Det typiska arbetsflödet när du arbetar med Git lokalt ser ut så här:

  1. Gör ändringar i dina projektfiler.
  2. Kontrollera status med git status för att se vilka filer som ändrats.
  3. Lägg till ändringar i Staging Area med git add <filnamn> eller git add ..
  4. Spara ändringarna permanent med git commit -m "Beskrivande meddelande".
  5. (Upprepa)

Du kan när som helst använda git log för att se historiken och git status för att se det aktuella läget.

Dessa kommandon (init, status, add, commit, log) utgör grunden för att arbeta med Git. I nästa avsnitt tittar vi på hur vi kan koppla vårt lokala repository till en fjärrserver som GitHub.

Arbeta med GitHub: Ditt projekt i molnet

Vi har lärt oss hur Git fungerar lokalt på vår dator för att spåra ändringar. Men den verkliga styrkan med Git, särskilt för samarbete och säkerhetskopiering, kommer när vi kopplar vårt lokala repository till en fjärrserver (remote repository). Den mest populära plattformen för detta är GitHub.

Mål:
Förstå hur du kopplar ett lokalt Git-repository till GitHub och använder de grundläggande kommandona för att skicka (push) och hämta (pull) ändringar.

Förutsättning:
Du har ett gratis konto på GitHub. Om inte, skapa ett nu.


Vad är GitHub?

GitHub är en webbaserad plattform för att lagra och hantera Git-repositories i molnet. Utöver lagring erbjuder GitHub:

  • Kodbackup: Säkert lagra dina projekt online.
  • Samarbete: Arbeta tillsammans i team, granska kod (Pull Requests) och diskutera ändringar.
  • Projekthantering: Hantera ärenden (Issues), projekt-tavlor (Projects) och wikis.
  • Versionshantering: Grafiskt gränssnitt för att se historik, jämföra versioner och utforska koden.
  • Open Source: Hem för miljontals öppen källkods-projekt.

Skapa ett fjärr-repository på GitHub

  1. Logga in på ditt GitHub-konto.
  2. Klicka på "+"-ikonen uppe till höger och välj New repository.
  3. Ge repositoryt ett namn (t.ex. mitt-projekt). Det är praktiskt om det matchar din lokala mapp, men det är inget krav.
  4. Lägg till en kort beskrivning (valfritt).
  5. Välj om det ska vara Public (synligt för alla) eller Private (endast för dig och inbjudna).
  6. Viktigt: Om du redan har ett lokalt repository, bocka INTE i rutorna för "Add a README file", "Add .gitignore" eller "Choose a license". Om du skapar ett helt nytt projekt direkt på GitHub kan du bocka i dessa.
  7. Klicka på Create repository.

Koppla ditt lokala repo till GitHub

När du har skapat repositoryt på GitHub visas instruktioner. Leta efter avsnittet "…or push an existing repository from the command line".

Steg 1: Lägg till fjärr-repository

git remote add origin https://github.com/ditt-anvandarnamn/mitt-projekt.git
  • origin är standardnamnet för din fjärrserver.
  • Byt ut URL:en mot den du får från GitHub.

Steg 2: Byt namn på huvudbranch (om nödvändigt)

git branch -M main
  • Byter namn på din huvudbranch till main (standard på GitHub).

Steg 3: Skicka upp din kod

git push -u origin main
  • Skickar dina commits till GitHub.
  • -u kopplar din lokala branch till fjärr-branchen, så att du senare kan använda bara git push och git pull.

Du kan behöva logga in eller autentisera dig mot GitHub (t.ex. med en personlig access token).


Skicka fler ändringar: git push

När du gjort nya ändringar och committat dem:

git push
  • Skickar alla nya commits på din nuvarande branch till GitHub.

Hämta ändringar: git pull

Om någon annan (eller du själv från en annan dator) har pushat ändringar till GitHub, hämta dem så här:

git pull
  • Hämtar och försöker automatiskt sammanfoga (merge) ändringarna från GitHub till din lokala kod.
  • Kör gärna git pull innan du börjar arbeta och innan du pushar egna ändringar.

Klona ett repository: git clone

Om du vill börja arbeta med ett projekt som redan finns på GitHub:

git clone https://github.com/anvandarnamn/projekt.git
  • Laddar ner hela repositoryt (alla filer och historik) till en ny mapp på din dator.
  • Sätter automatiskt upp origin så att du kan använda git push och git pull.

Sammanfattning

GitHub är en plattform för att lagra och samarbeta kring Git-repositories i molnet. Genom att koppla ditt lokala repo till GitHub kan du:

  • Skicka ändringar till GitHub med git push.
  • Hämta ändringar från GitHub med git pull.
  • Klona befintliga projekt med git clone.

Att använda Git tillsammans med GitHub är standard inom modern webbutveckling och gör det enkelt att samarbeta, säkerhetskopiera och visa upp dina projekt.

Nu är det dags att öva på dessa HTML- och Git-färdigheter!

Mer Git-kommandon

Introduktion till avancerade Git-tekniker

Nu när du behärskar grunderna i Git är det dags att utforska mer avancerade kommandon och tekniker. Dessa verktyg ger dig större kontroll över ditt versionshanteringssystem och gör dig till en mer effektiv utvecklare.

graph TD
    A[Git Grundläggande] --> B[Branching & Checkout]
    A --> C[Merge & Rebase]
    A --> D[Konflikthantering]
    A --> E[Konfiguration]
    
    B --> B1[git checkout]
    B --> B2[git switch]
    B --> B3[Detached HEAD]
    
    C --> C1[git merge]
    C --> C2[git rebase]
    C --> C3[Fast-forward]
    
    D --> D1[Merge conflicts]
    D --> D2[Konfliktlösning]
    D --> D3[Merge tools]
    
    E --> E1[git config]
    E --> E2[Globala inställningar]
    E --> E3[SSH-nycklar]

Git Checkout - Navigera mellan brancher och commits

git checkout är ett mångsidigt kommando som låter dig "hoppa" mellan olika versioner av ditt projekt.

Växla mellan brancher

# Visa alla brancher
git branch

# Skapa och växla till ny branch
git checkout -b ny-funktion

# Växla till befintlig branch
git checkout main
git checkout utveckling

# Kort för att skapa branch från specifik commit
git checkout -b bugfix-123 abc1234

Checkout till specifika commits

# Visa commit-historik
git log --oneline

# Gå till specifik commit (skapar "detached HEAD")
git checkout abc1234

# Gå tillbaka till senaste commit på current branch
git checkout main

Detached HEAD State

När du gör checkout till en specifik commit hamnar du i "detached HEAD" tillstånd:

graph LR
    A[Commit A] --> B[Commit B]
    B --> C[Commit C]
    C --> D[Commit D]
    
    HEAD -->|Normal| D
    HEAD2[HEAD detached] -->|git checkout B| B
    
    subgraph "Normal state"
        HEAD
        D
    end
    
    subgraph "Detached HEAD"
        HEAD2
        B
    end
# Om du är i detached HEAD och vill behålla ändringar
git switch -c ny-branch-namn

# Eller gå tillbaka utan att spara ändringar
git switch main

Återställa filer med checkout

# Återställ en specifik fil till senaste commit
git checkout -- fil.txt

# Återställ fil till specifik commit
git checkout abc1234 -- fil.txt

# Återställ alla filer i katalog
git checkout -- .

Git Switch - Modernare sätt att växla branches

git switch introducerades i Git 2.23 som ett tydligare alternativ till git checkout för branch-hantering:

# Växla till befintlig branch
git switch main
git switch utveckling

# Skapa och växla till ny branch
git switch -c ny-funktion

# Växla tillbaka till föregående branch
git switch -

# Återgå till main från detached HEAD
git switch main

Fördelar med git switch:

  • Tydligare syfte (endast för brancher)
  • Säkrare (mindre risk för misstag)
  • Modernare syntax

Merge och Rebase - Kombinera ändringar

Git Merge - Traditionell sammanfogning

# Gå till target branch (oftast main)
git switch main

# Hämta senaste ändringar
git pull origin main

# Merge in branch
git merge funktions-branch

# Push merged result
git push origin main

Merge-strategier visualiserade

graph LR
    A[Commit A] --> B[Commit B]
    B --> C[Commit C]
    
    A --> D[Commit D]
    D --> E[Commit E]
    
    C --> F[Merge Commit]
    E --> F
    
    subgraph "main branch"
        A
        B
        C
        F
    end
    
    subgraph "feature branch"
        D
        E
    end

Fast-forward merge

När target branch inte har nya commits:

# Main branch har inga nya commits sedan branch skapades
git merge funktions-branch
# Resulterar i fast-forward (ingen merge commit)
graph LR
    A[Commit A] --> B[Commit B]
    B --> C[Commit C]
    C --> D[Commit D]
    
    subgraph "Före merge"
        A
        B
        MAIN[main] --> B
        C
        D
        FEATURE[feature] --> D
    end
    
    subgraph "Efter fast-forward"
        A2[Commit A] --> B2[Commit B]
        B2 --> C2[Commit C]
        C2 --> D2[Commit D]
        MAIN2[main] --> D2
    end

Git Rebase - Omskrivning av historik

Rebase "flyttar" dina commits till toppen av target branch:

# Gå till din feature branch
git switch funktions-branch

# Rebase mot main
git rebase main

# Om inga konflikter: push (kan kräva force)
git push --force-with-lease origin funktions-branch

Rebase vs Merge visualiserat

graph TD
    subgraph "Original state"
        A1[A] --> B1[B]
        A1 --> C1[C]
        C1 --> D1[D]
    end
    
    subgraph "Efter Merge"
        A2[A] --> B2[B]
        A2 --> C2[C] 
        C2 --> D2[D]
        B2 --> M[Merge commit]
        D2 --> M
    end
    
    subgraph "Efter Rebase"
        A3[A] --> B3[B]
        B3 --> C3[C']
        C3 --> D3[D']
    end

Interaktiv rebase

# Redigera de senaste 3 commits
git rebase -i HEAD~3

# Öppnar editor med alternativ:
# pick = använd commit
# reword = ändra commit-meddelande  
# edit = redigera commit
# squash = kombinera med föregående commit
# drop = ta bort commit

När ska du använda vad?

  • Merge: Bevarar historik, säkrare, bra för publika branches
  • Rebase: Renare historik, linjär utveckling, bra för feature branches

Konflikthantering (Merge Conflicts)

Konflikter uppstår när Git inte kan automatiskt kombinera ändringar.

Förstå konflikter

# När merge skapar konflikter
git merge funktions-branch
# Output: CONFLICT (content): Merge conflict in fil.txt

Konfliktmarkering i filer

// fil.txt efter konflikt
function calculateTotal(items) {
<<<<<<< HEAD
    return items.reduce((sum, item) => sum + item.price, 0);
=======
    return items.reduce((total, item) => total + item.cost, 0);
>>>>>>> funktions-branch
}

Förklaring:

  • <<<<<<< HEAD: Början på dina ändringar (current branch)
  • =======: Skiljelinje mellan versioner
  • >>>>>>> branch-name: Slut på den andra branchens ändringar

Lösa konflikter

Manuell lösning

# 1. Öppna fil och redigera manuellt
# Ta bort konfliktmarkeringar och välj önskad version

# 2. Lägg till den lösta filen
git add fil.txt

# 3. Slutför merge
git commit
# (eller för rebase: git rebase --continue)

Använda merge tools

# Konfigurera merge tool (en gång)
git config --global merge.tool vimdiff
# eller: vscode, meld, kdiff3, etc.

# Starta merge tool vid konflikt
git mergetool

# Efter lösning av alla konflikter
git commit

Förebygga konflikter

# Håll din branch uppdaterad
git switch main
git pull origin main
git switch funktions-branch
git rebase main  # eller git merge main

# Små, frekventa commits
# Kommunicera med teamet om stora ändringar
# Använd .gitattributes för specifika filtyper

Git Config - Anpassa din Git-miljö

Nivåer av konfiguration

Git har tre nivåer av konfiguration:

# System-nivå (alla användare)
git config --system

# Global nivå (din användare)
git config --global

# Repository-nivå (endast detta projekt)
git config --local

Grundläggande konfiguration

# Sätt användarinfo (obligatoriskt)
git config --global user.name "Ditt Namn"
git config --global user.email "din.email@example.com"

# Standardeditor
git config --global core.editor "code --wait"  # VS Code
git config --global core.editor "vim"          # Vim
git config --global core.editor "nano"         # Nano

# Standardbranch-namn
git config --global init.defaultBranch main

# Radslut-hantering (viktigt för Windows/Mac/Linux)
git config --global core.autocrlf input     # Mac/Linux
git config --global core.autocrlf true      # Windows

Alias för vanliga kommandon

# Skapa användbara alias
git config --global alias.st status
git config --global alias.co checkout
git config --global alias.br branch
git config --global alias.ci commit

# Avancerade alias
git config --global alias.unstage 'reset HEAD --'
git config --global alias.last 'log -1 HEAD'
git config --global alias.visual '!gitk'

# Vacker log output
git config --global alias.lg "log --oneline --decorate --graph --all"

# Använd alias
git st        # istället för git status
git lg        # vacker log output

Konfigurationsfil

# Visa all konfiguration
git config --list

# Visa var konfiguration kommer ifrån
git config --list --show-origin

# Redigera global konfiguration direkt
git config --global --edit

Exempel på .gitconfig fil:

[user]
    name = Ditt Namn
    email = din.email@example.com

[core]
    editor = code --wait
    autocrlf = input

[init]
    defaultBranch = main

[alias]
    st = status
    co = checkout
    br = branch
    ci = commit
    lg = log --oneline --decorate --graph --all

[push]
    default = simple

[pull]
    rebase = false

SSH-nycklar för GitHub/GitLab

Generera SSH-nyckel

# Generera ny SSH-nyckel
ssh-keygen -t ed25519 -C "din.email@example.com"

# För äldre system som inte stödjer ed25519
ssh-keygen -t rsa -b 4096 -C "din.email@example.com"

# Starta SSH agent
eval "$(ssh-agent -s)"

# Lägg till nyckel till agent
ssh-add ~/.ssh/id_ed25519

Konfiguration för GitHub

# Kopiera publik nyckel (Mac)
pbcopy < ~/.ssh/id_ed25519.pub

# Linux
cat ~/.ssh/id_ed25519.pub

# Lägg till i GitHub under Settings > SSH and GPG keys

# Testa anslutningen
ssh -T git@github.com

Avancerade Git-kommandon för nyfikna

Git Cherry-pick

Välj specifika commits från andra branches:

# Applicera en specifik commit på current branch
git cherry-pick abc1234

# Cherry-pick flera commits
git cherry-pick abc1234 def5678

# Cherry-pick ett intervall
git cherry-pick abc1234..def5678
graph LR
    A[A] --> B[B]
    A --> C[C]
    C --> D[D]
    D --> E[E]
    
    B --> F[F]
    F --> G[G]
    
    G --> H[D']
    
    subgraph "main branch"
        A
        B
        F
        G
        H
    end
    
    subgraph "feature branch"
        C
        D
        E
    end

Git Stash - Temporär lagring

# Spara ändringar temporärt
git stash

# Spara med meddelande
git stash push -m "Work in progress på login"

# Lista alla stashes
git stash list

# Applicera senaste stash
git stash pop

# Applicera specifik stash
git stash apply stash@{2}

# Ta bort stash utan att applicera
git stash drop stash@{1}

Git Worktree - Flera arbetsytor

# Skapa ny worktree för branch
git worktree add ../projekt-hotfix hotfix-branch

# Lista alla worktrees
git worktree list

# Ta bort worktree
git worktree remove ../projekt-hotfix

Git Reflog - Återhämtning

# Visa vad som hänt med HEAD
git reflog

# Återställ till tidigare state
git reset --hard HEAD@{2}

# Hitta "förlorade" commits
git reflog --all

Git Bisect - Hitta buggar

# Starta bisect-session
git bisect start

# Markera aktuell commit som dålig
git bisect bad

# Markera tidigare bra commit
git bisect good abc1234

# Git checkar ut commit i mitten - testa och markera
git bisect bad    # eller git bisect good

# Fortsätt tills Git hittar problematisk commit
git bisect reset  # avsluta

Git Hooks - Automatisering

# Navigera till hooks-mappen
cd .git/hooks

# Skapa pre-commit hook
nano pre-commit

# Exempel: Kör tests innan commit
#!/bin/bash
npm test
if [ $? -ne 0 ]; then
    echo "Tests failed, commit aborted"
    exit 1
fi

# Gör executable
chmod +x pre-commit

Git Submodules - Externa repositories

# Lägg till submodule
git submodule add https://github.com/user/repo.git libs/external

# Klona projekt med submodules
git clone --recurse-submodules https://github.com/user/project.git

# Uppdatera submodules
git submodule update --remote

Bästa praxis och tips

Commit-meddelanden

# Bra struktur för commit-meddelanden
# <typ>: <kort beskrivning>
# 
# <längre beskrivning vid behov>
# 
# <referenser till issues>

# Exempel:
feat: add user authentication system

Implement login/logout functionality with JWT tokens.
Includes password hashing and session management.

Fixes #123

Branch-namngivning

# Bra branch-namns konventioner
feature/user-authentication
bugfix/login-error-handling
hotfix/security-vulnerability
release/v2.1.0

Git Flow vs GitHub Flow

Git Flow - Komplexare, strukturerad:

  • main - produktionskod
  • develop - utvecklingsbranch
  • feature/* - nya funktioner
  • release/* - release förberedelser
  • hotfix/* - kritiska fixes

GitHub Flow - Enklare, snabbare:

  • main - alltid deploybar
  • feature/* - korta feature branches
  • Pull requests för review
  • Deploy direkt från main

Felsökning och problemlösning

Vanliga problem och lösningar

Undo senaste commit (behåll ändringar)

git reset --soft HEAD~1

Undo senaste commit (ta bort ändringar)

git reset --hard HEAD~1

Ändra senaste commit-meddelande

git commit --amend -m "Nytt meddelande"

Lägg till fil till senaste commit

git add glömd-fil.txt
git commit --amend --no-edit

Återställa fil till specifik version

git checkout abc1234 -- fil.txt

Push refuseras (diverged history)

# Se vad som skiljer
git log --oneline origin/main..main

# Option 1: Pull och merge
git pull origin main

# Option 2: Pull och rebase
git pull --rebase origin main

# Option 3: Force push (FARLIGT - använd bara på egna branches)
git push --force-with-lease origin main

Debug Git-kommandon

# Visa vad Git gör
export GIT_TRACE=1
git status

# Debug merge
export GIT_MERGE_VERBOSITY=2
git merge branch-name

# Visa konfiguration
git config --list --show-origin

Sammanfattning

Du har nu lärt dig avancerade Git-tekniker som gör dig till en mer kompetent utvecklare:

  • Checkout och Switch: Navigera mellan branches och commits säkert
  • Merge vs Rebase: Förstå när och hur du ska kombinera ändringar
  • Konflikthantering: Lösa merge conflicts effektivt
  • Konfiguration: Anpassa Git för din arbetsrutation
  • Avancerade verktyg: Cherry-pick, stash, reflog och mer

Nästa steg

  • Experimentera med dessa kommandon i test-repositories
  • Lär dig Git hooks för automatisering
  • Utforska Git GUIs som Sourcetree eller GitKraken
  • Fördjupa dig i Git internals för ännu djupare förståelse

Git är ett kraftfullt verktyg - med dessa kunskaper kan du hantera även komplexa utvecklingsprojekt med självförtroende!

Praktiska övningar: HTML och Git

Teori är bra, men det bästa sättet att lära sig HTML och Git är att använda dem! Här är några övningar för att befästa kunskaperna från detta kapitel.

Mål:
Praktiskt tillämpa HTML-taggar för struktur och semantik, samt använda grundläggande Git-kommandon för att spåra och spara ändringar lokalt och på GitHub.

Förutsättningar:
Du har VS Code (eller annan editor), Git och en terminal installerad. Du har också ett GitHub-konto.


Övning 1: Skapa en enkel "Om Mig"-sida

  1. Skapa projektmapp:
    Skapa en ny mapp, t.ex. om-mig-sida.

  2. Initiera Git:
    Öppna terminalen, navigera till mappen (cd om-mig-sida) och kör:

    git init
    
  3. Skapa HTML-fil:
    Skapa en fil med namnet index.html i mappen.

  4. Grundstruktur:
    Lägg till grundläggande HTML5-struktur i index.html:

    • <!DOCTYPE html>
    • <html lang="sv">
    • <head> med <meta charset="UTF-8"> och <title>
    • <body>

    Ge sidan en passande titel, t.ex. "Om [Ditt Namn]".

  5. Lägg till innehåll i <body>:

    • En huvudrubrik (<h1>) med ditt namn.
    • En kort paragraf (<p>) som introducerar dig.
    • En underrubrik (<h2>) för "Mina intressen".
    • En oordnad lista (<ul>) med några av dina intressen (<li>).
    • En underrubrik (<h2>) för "Kontakt".
    • En paragraf (<p>) med en länk (<a>) till din (påhittade eller riktiga) e-postadress (href="mailto:din.epost@example.com").
    • (Valfritt) En bild (<img>) på dig själv eller något relaterat. Glöm inte alt-attributet!
  6. Första commit:

    git status
    git add index.html
    git commit -m "Skapa grundläggande Om Mig-sida med innehåll"
    
  7. Validera (bonus):
    Klistra in din HTML-kod i W3C Markup Validation Service för att kontrollera att den är korrekt.

  8. Visa i webbläsare:
    Öppna index.html i din webbläsare.


Övning 2: Semantisk struktur och fler ändringar

  1. Fortsätt i samma projekt.

  2. Lägg till semantiska element:

    • Omslut rubriken och introduktionen med <header>.
    • Omslut huvudinnehållet (intressen, kontakt) med <main>.
    • Lägg till en <footer> längst ner, t.ex. med copyright.
    • Om du har flera tydliga delar i <main>, omslut dem med <section> och ge varje sektion en egen rubrik (<h2>).
  3. Commit:

    git status
    git add .
    git commit -m "Lägg till semantisk struktur (header, main, footer, sections)"
    
  4. Visa historik:

    git log --oneline
    

Övning 3: Koppla till GitHub

  1. Skapa repo på GitHub:
    Skapa ett nytt, tomt repository på GitHub (utan README, .gitignore eller license).

  2. Koppla lokalt till fjärr:
    Följ instruktionerna från GitHub ("…or push an existing repository").
    Byt ut URL:en mot den från ditt repo:

    git remote add origin https://github.com/ditt-anvandarnamn/om-mig-sida.git
    git branch -M main
    git push -u origin main
    
  3. Verifiera:
    Gå till din repository-sida på GitHub och kontrollera att din index.html och dina commits syns.

  4. Gör en liten ändring:
    Lägg till ytterligare ett intresse i listan i index.html.

  5. Commit och push:

    git status
    git add .
    git commit -m "Lägg till ett till intresse"
    git push
    
  6. Verifiera igen:
    Uppdatera sidan på GitHub och kontrollera att ändringen syns.


Sammanfattning och nästa steg

Genom dessa övningar har du praktiskt skapat en enkel HTML-sida, strukturerat den semantiskt och använt Git för att spåra ändringar lokalt och synkronisera dem med ett fjärr-repository på GitHub. Detta är grunden för arbetsflödet vi kommer använda genom resten av kursen.

I nästa kapitel introducerar vi CSS för att börja styla våra HTML-sidor och ge dem ett visuellt utseende.

Tekniska Intervjufrågor: Git och Versionshantering

Detta avsnitt innehåller exempel på tekniska intervjufrågor som kan dyka upp gällande Git och versionshantering. Frågorna är utformade för att testa både teoretisk förståelse och praktisk kunskap.

Använd dessa frågor för att testa din kunskap och förbereda dig för tekniska intervjuer.


Fråga 1: Grundläggande Git-koncept

Fråga: "Förklara skillnaden mellan en 'commit', en 'branch' och ett 'repository' i Git. Ge ett praktiskt exempel på när du skulle använda var och en."

Förslag till svar:

  • Repository: En mapp som innehåller alla filer och hela versionshistoriken för ett projekt. Det är som projektets "hem" med all information Git behöver.
  • Commit: En sparad ögonblicksbild av projektet vid en specifik tidpunkt, med ett meddelande som beskriver vad som ändrats. Som att ta ett foto av projektets tillstånd.
  • Branch: En parallell utvecklingslinje som låter dig arbeta på nya funktioner utan att påverka huvudkoden. Som att skapa en kopia att experimentera på.

Exempel: Du har ett repository för en webbsida. Du skapar en branch för att lägga till en ny funktion, gör flera commits medan du utvecklar, och när du är klar mergar du tillbaka till huvudbranchen.


Fråga 2: Git-arbetsflöde

Fråga: "Beskriv steg-för-steg vad som händer när du kör kommandona: git add ., git commit -m "Fix bug", och git push origin main."

Förslag till svar:

  1. git add .: Lägger till alla ändrade filer till staging area (mellanlagret). Filerna är nu förberedda för commit men inte än sparade.
  2. git commit -m "Fix bug": Skapar en ny commit med meddelandet "Fix bug" som innehåller alla filer från staging area. Ändringarna är nu sparade i lokal versionshistorik.
  3. git push origin main: Skickar den nya commiten från din lokala main-branch till remote repository (t.ex. GitHub) så andra kan se dina ändringar.

Fråga 3: Staging Area

Fråga: "Varför finns staging area i Git? Vad är skillnaden mellan working directory, staging area och repository?"

Förslag till svar: Staging area låter dig välja exakt vilka ändringar som ska inkluderas i nästa commit. Detta ger kontroll och flexibilitet.

  • Working Directory: Filerna du ser och redigerar i din projektmapp
  • Staging Area: Mellanlagret där du förbereder ändringar för commit
  • Repository: Den sparade versionshistoriken i .git-mappen

Fördel: Du kan arbeta på flera funktioner samtidigt men committa dem separat genom att bara lägga till relevanta filer till staging area.


Fråga 4: Konflikthantering

Fråga: "Du försöker göra en git pull men får meddelandet 'merge conflict'. Vad har hänt och hur löser du det?"

Förslag till svar: Vad som hänt: Någon annan har ändrat samma rader i samma fil som du också ändrat. Git vet inte vilken version som ska behållas.

Lösning:

  1. Öppna den konfliktmarkerade filen
  2. Leta efter <<<<<<<, =======, och >>>>>>> markeringar
  3. Bestäm vilken kod som ska behållas (eller kombinera)
  4. Ta bort konfliktmarkeringarna
  5. git add den fixade filen
  6. git commit för att slutföra merge

Fråga 5: Git Log och Historik

Fråga: "Du behöver hitta när en specifik bug introducerades i koden. Vilka Git-kommandon skulle du använda och varför?"

Förslag till svar:

  1. git log --oneline: Se en översikt av alla commits
  2. git log --grep="keyword": Sök i commit-meddelanden
  3. git blame filename: Se vem som ändrade varje rad och när
  4. git bisect: Binärsökning för att hitta exakt vilken commit som introducerade buggen
  5. git show commitID: Se exakt vad som ändrades i en specifik commit

Git bisect är särskilt kraftfullt för att hitta när buggar introducerades.


Fråga 6: Ångra ändringar

Fråga: "Du har råkat commita fel kod till din lokala repository men inte pushat än. Hur kan du ångra den senaste commiten och vad är skillnaden mellan de olika sätten?"

Förslag till svar:

  1. git reset --soft HEAD~1: Ångrar commiten men behåller ändringarna i staging area
  2. git reset --mixed HEAD~1: (default) Ångrar commiten och tar bort från staging, men behåller ändringarna i working directory
  3. git reset --hard HEAD~1: Ångrar commiten och tar bort alla ändringar helt
  4. git revert HEAD: Skapar en ny commit som ångrar den senaste commiten (säkrare om redan pushat)

Regel: Använd reset bara för lokala commits, revert för commits som redan är pushade.


Fråga 7: .gitignore

Fråga: "Du arbetar på ett Node.js-projekt och märker att node_modules-mappen committas av misstag. Hur fixar du detta och förhindrar att det händer igen?"

Förslag till svar: För att fixa:

  1. Skapa .gitignore-fil i projektets root
  2. Lägg till node_modules/ i filen
  3. git rm -r --cached node_modules/ (tar bort från Git utan att radera lokalt)
  4. git commit -m "Remove node_modules and add to gitignore"

Andra vanliga ignore-patterns:

node_modules/
.env
*.log
dist/
.DS_Store

.gitignore ska committas så hela teamet har samma ignore-regler.


Fråga 8: HTML5 Grundstruktur

Fråga: "Förklara vad varje del gör i denna HTML5-struktur och varför de är viktiga:"

<!DOCTYPE html>
<html lang="sv">
<head>
    <meta charset="UTF-8">
    <title>Min Sida</title>
</head>
<body>
    <h1>Välkommen</h1>
</body>
</html>

Förslag till svar:

  • <!DOCTYPE html>: Deklarerar att dokumentet använder HTML5-standarden. Måste vara först.
  • <html lang="sv">: Rotelementet som omsluter allt innehåll. lang="sv" anger svenska som huvudspråk för tillgänglighet och SEO.
  • <head>: Innehåller metadata som inte visas direkt - information för webbläsaren och sökmotorer.
  • <meta charset="UTF-8">: Anger teckenkodning för att stödja internationella tecken (å, ä, ö). Kritiskt viktigt!
  • <title>: Titeln som visas i webbläsarens flik och används av sökmotorer.
  • <body>: Allt synligt innehåll på sidan.

Fråga 9: HTML-Element och Attribut

Fråga: "Förklara skillnaden mellan element, taggar och attribut. Ge exempel på när du skulle använda <a>, <img> och <ul> elementen."

Förslag till svar:

  • Taggar: Koder inom < > som markerar början (<p>) och slut (</p>) på element
  • Element: Kompletta konstruktioner med starttagg, innehåll och sluttagg: <p>Text</p>
  • Attribut: Extra information i starttaggen: <a href="länk">text</a>

Exempel:

  • <a href="https://google.com">Sök</a>: För att skapa klickbara länkar till andra sidor
  • <img src="bild.jpg" alt="Beskrivning">: För att visa bilder (tomt element, ingen sluttagg)
  • <ul><li>Punkt 1</li><li>Punkt 2</li></ul>: För punktlistor där ordningen inte spelar roll

Fråga 10: Semantisk HTML vs Generiska Element

Fråga: "Vad är skillnaden mellan dessa två kodexempel och varför är det ena bättre än det andra?"

<!-- Exempel A -->
<div id="header">
    <div class="navigation">Meny</div>
</div>
<div class="content">Innehåll</div>

<!-- Exempel B -->
<header>
    <nav>Meny</nav>
</header>
<main>Innehåll</main>

Förslag till svar: Exempel B är bättre eftersom det använder semantiska HTML5-element:

Fördelar med semantisk HTML:

  • Tillgänglighet: Skärmläsare kan navigera bättre med <nav>, <main> etc.
  • SEO: Sökmotorer förstår innehållets struktur och syfte
  • Läsbarhet: Kod blir självförklarande utan att behöva läsa id/class-attribut
  • Framtidssäker: Webbstandarder föredrar semantiska element

<div> ska bara användas för styling/layout när inget semantiskt element passar.


Fråga 11: HTML5 Semantiska Element

Fråga: "Du ska bygga en bloggsida. Förklara hur du skulle använda <header>, <nav>, <main>, <article>, <aside> och <footer> för att strukturera sidan."

Förslag till svar:

<header>
    <h1>Min Blogg</h1>
    <nav>
        <ul>
            <li><a href="/">Hem</a></li>
            <li><a href="/om">Om</a></li>
        </ul>
    </nav>
</header>

<main>
    <article>
        <h2>Blogginläggets titel</h2>
        <p>Innehållet i blogginlägget...</p>
    </article>
    
    <aside>
        <h3>Populära inlägg</h3>
        <ul>Relaterade länkar...</ul>
    </aside>
</main>

<footer>
    <p>&copy; 2024 Min Blogg</p>
</footer>

Varför denna struktur:

  • <header>: Sidans topp med titel och huvudnavigering
  • <main>: Huvudinnehållet (bara ett per sida)
  • <article>: Fristående innehåll som kan distribueras separat
  • <aside>: Kompletterande information vid sidan av huvudinnehållet
  • <footer>: Sidfot med copyright och kontaktinfo

Fråga 12: Tillgänglighet och SEO

Fråga: "Varför är alt-attributet viktigt för bilder och hur påverkar semantisk HTML sökmotoroptimering?"

Förslag till svar: alt-attributet för bilder:

  • Tillgänglighet: Skärmläsare läser upp alt-texten för synskadade användare
  • Fallback: Visas om bilden inte kan laddas (långsam anslutning, fel URL)
  • SEO: Hjälper sökmotorer förstå bildinnehållet
<img src="produktbild.jpg" alt="Röd t-shirt i bomull, storlek M">

Semantisk HTML för SEO:

  • <h1>-<h6>: Hjälper sökmotorer förstå innehållshierarki
  • <article>: Identifierar huvudinnehåll som är värt att indexera
  • <nav>: Visar sidans navigationsstuktur
  • Strukturerad data: Semantiska element gör det lättare för sökmotorer att tolka sidan korrekt

Resultat: Bättre ranking i sökresultat och ökad tillgänglighet.


Tips för Tekniska Intervjuer

  • Förklara din tankeprocess medan du svarar
  • Använd konkreta exempel från egna projekt när möjligt
  • Erkänn om du inte vet och förklara hur du skulle ta reda på svaret
  • Ställ följdfrågor för att förtydliga vad intervjuaren söker

Kapitel 3: Grundläggande CSS – Ge Stil åt Webbens Struktur

I det föregående kapitlet lade vi grunden med HTML, webbsidornas skelett, och lärde oss hantera koden med Git. Men enbart HTML ger oss ganska tråkiga och ostrukturerade sidor. Hur får vi dem att se bra ut? Hur kontrollerar vi färger, typsnitt, layout och anpassar utseendet för olika skärmstorlekar?

Svaret är CSS (Cascading Style Sheets). CSS är språket vi använder för att beskriva presentationen och stilen för HTML-dokument. Om HTML är skelettet, är CSS kläderna, sminket och frisyren – det som ger sidan dess visuella identitet.

Motivation:
CSS gör det möjligt att separera struktur och utseende, vilket leder till renare kod, bättre underhåll och mer flexibla webbplatser. Med CSS kan du skapa allt från enkla färgändringar till avancerade, responsiva layouter.


Varför är CSS viktigt?

  • Separation of Concerns: CSS låter oss separera innehållets struktur (HTML) från dess presentation (CSS). Detta gör koden renare, lättare att underhålla och mer flexibel.
  • Visuell design: CSS ger oss kontroll över allt från färger och typsnitt till komplexa layouter och animationer.
  • Responsivitet: Med CSS kan vi skapa webbplatser som anpassar sig och ser bra ut på alla typer av enheter – från stora datorskärmar till mobiler och surfplattor.
  • Effektivitet: Genom att definiera stilar på ett ställe och återanvända dem kan vi enkelt uppdatera utseendet på hela webbplatser med små ändringar.

Vad kommer du att lära dig i detta kapitel?

  • Introduktion till CSS: Vad CSS är och hur du kopplar det till HTML.
  • Selektorer, färger och typografi: Hur du väljer ut HTML-element (selektorer) och applicerar grundläggande stilar som färger och typsnitt.
  • Box model och layout: Hur varje HTML-element kan ses som en låda (box model) och hur du kan kontrollera dess dimensioner, marginaler (margin) och utfyllnad (padding) för att skapa layouter.
  • Responsiv design: Grunderna i hur du använder Media Queries för att anpassa stilar baserat på skärmstorlek.
  • Mobile-first design: Principen att designa för mobilen först och sedan skala upp för större skärmar.
  • Praktiska övningar: Du får applicera CSS-regler för att styla den "Om Mig"-sida du skapade i förra kapitlet.

Låt oss börja ge våra HTML-sidor lite färg och form!

Introduktion till CSS: Syntax och inkludering

CSS står för Cascading Style Sheets. Det är språket som webbläsare använder för att bestämma hur HTML-element ska visas visuellt. Utan CSS skulle webben vara en ganska monoton plats med bara svart text på vit bakgrund.

Motivation:
CSS gör det möjligt att separera innehåll (HTML) från utseende (design). Det gör webbsidor snyggare, mer lättlästa och enklare att underhålla.


CSS-syntax: Regler som styr utseendet

En CSS-regel består huvudsakligen av två delar:

  1. Selektor (selector): Anger vilket eller vilka HTML-element som regeln ska appliceras på.
  2. Deklarationsblock (declaration block): Innehåller en eller flera deklarationer, separerade med semikolon (;). Blocket omges av måsvingar ({ }).
    • Deklaration (declaration): Består av en egenskap (property) och ett värde (value), separerade med kolon (:). Egenskapen är det du vill ändra (t.ex. color, font-size, background-color), och värdet anger hur du vill ändra det (t.ex. blue, 16px, #FFFFFF).
/* Detta är en CSS-kommentar */

selector {
  property: value;
  another-property: another-value;
}

Exempel:
Låt oss säga att vi vill att alla paragrafer (<p>) ska ha blå text och en textstorlek på 16 pixlar.

p {
  color: blue;        /* Gör texten blå */
  font-size: 16px;    /* Sätter textstorleken till 16 pixlar */
}
  • Selektor: p (väljer alla <p>-element)
  • Deklarationsblock: { color: blue; font-size: 16px; }
  • Deklarationer: color: blue; och font-size: 16px;
  • Egenskaper: color och font-size
  • Värden: blue och 16px

Hur kopplar man CSS till HTML?

Det finns tre huvudsakliga sätt att applicera CSS-regler på ett HTML-dokument:

1. Extern CSS-fil (external stylesheet) – Rekommenderat!

Du skriver dina CSS-regler i en separat fil (t.ex. style.css) och länkar in den i HTML-filen:

<link rel="stylesheet" href="style.css">

Exempel på innehåll i style.css:

body {
  font-family: sans-serif;
}
p {
  color: #333;
}
h1 {
  color: darkcyan;
}

Exempel på HTML:

<!DOCTYPE html>
<html lang="sv">
<head>
    <meta charset="UTF-8">
    <title>Min Sida</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <h1>Välkommen</h1>
    <p>Detta är min webbsida.</p>
</body>
</html>

Fördelar:

  • Separation: Håller struktur (HTML) och stil (CSS) helt åtskilda.
  • Underhåll: Lätt att ändra utseendet på hela webbplatsen genom att redigera en enda fil.
  • Prestanda: Webbläsaren kan cacha (spara en lokal kopia av) .css-filen, vilket snabbar upp laddningen av efterföljande sidor.

Nackdelar: Kräver en extra fil och en extra HTTP-förfrågan (request) för att ladda CSS-filen (men detta vägs oftast upp av cachning).


2. Intern CSS (internal stylesheet)

Du skriver CSS-regler i en <style>-tagg i <head> på HTML-sidan:

<!DOCTYPE html>
<html lang="sv">
<head>
    <meta charset="UTF-8">
    <title>Min Sida</title>
    <style>
      body {
        font-family: sans-serif;
      }
      p {
        color: #333;
      }
      h1 {
        color: darkcyan;
      }
    </style>
</head>
<body>
    <h1>Välkommen</h1>
    <p>Detta är min webbsida.</p>
</body>
</html>

Fördelar:
Allt finns i en enda fil, kan vara användbart för små, enstaka sidor eller för att snabbt testa något.

Nackdelar:
Blandar struktur och stil, svårare att underhålla på större webbplatser, ingen cachning av stilarna mellan olika sidor.


3. Inline CSS (inline styles)

Du skriver CSS direkt i HTML-elementets style-attribut:

<p style="color: #333; font-size: 14px;">Detta är min webbsida med inline-stil.</p>

Fördelar:
Kan snabbt applicera en unik stil på ett enda element.

Nackdelar:

  • Används sällan och bör undvikas!
  • Blandar struktur och stil maximalt.
  • Mycket svårt att underhålla och återanvända stilar.
  • Gör HTML-koden rörig.
  • Har högst specificitet (se nedan), vilket kan göra det svårt att skriva över stilar i externa filer.
  • Säkerhetsaspekt: Inline-stilar kan ibland utnyttjas för XSS-attacker (cross-site scripting) om användarinnehåll får skrivas direkt till style-attributet.

Cascading och specificitet (kort introduktion)

Namnet "Cascading" Style Sheets antyder en viktig mekanism: kaskaden. Om flera CSS-regler matchar samma element och försöker sätta samma egenskap, hur bestämmer webbläsaren vilken regel som vinner?

Detta avgörs av en kombination av:

  1. Ursprung (origin): Stilar från webbplatsens utvecklare (author stylesheets) väger tyngre än webbläsarens standardstilar (user-agent stylesheets).
  2. Specificitet (specificity): Mer specifika selektorer väger tyngre än mindre specifika. T.ex. en regel för ett element med ett visst ID (#myId) är mer specifik än en regel för ett element med en viss klass (.myClass), som i sin tur är mer specifik än en regel för en elementtyp (p). Inline-stilar har högst specificitet.
  3. Ordning (order): Om specificiteten är densamma, vinner den regel som definieras senast i koden eller i den senast inlänkade CSS-filen.

Exempel: Hur cascading fungerar i praktiken

<p id="important" class="highlight">Detta är en viktig paragraf.</p>
/* Regel 1: Elementtyp-selektor (lägst specificitet) */
p {
  color: black;
  font-size: 14px;
}

/* Regel 2: Klass-selektor (högre specificitet) */
.highlight {
  color: blue;
  font-weight: bold;
}

/* Regel 3: ID-selektor (ännu högre specificitet) */
#important {
  color: red;
}

/* Regel 4: Samma ID-selektor, men definierad senare */
#important {
  color: green;
  text-decoration: underline;
}

Resultat:

  • Färg: grön (från regel 4 - samma specificitet som regel 3, men definierad senare)
  • Font-storlek: 14px (från regel 1 - ingen annan regel definierar detta)
  • Font-vikt: bold (från regel 2 - ingen regel med högre specificitet överskrider detta)
  • Text-decoration: underline (från regel 4)

Förklaring:

  • Regel 1 (p) har lägst specificitet men bidrar med font-size eftersom ingen annan regel definierar detta.
  • Regel 2 (.highlight) har högre specificitet än regel 1, så color: blue skulle vinna över color: black, men...
  • Regel 3 och 4 (#important) har högst specificitet och överskrider både regel 1 och 2 för färg.
  • Mellan regel 3 och 4 vinner regel 4 eftersom den definieras senare (samma specificitet).

Inline-stil skulle vinna över allt:
Om vi hade <p id="important" class="highlight" style="color: purple;">, skulle texten bli lila eftersom inline-stilar har högst specificitet.


Nyckelordet !important

I CSS kan du använda nyckelordet !important för att tvinga en deklaration att vinna över andra, oavsett specificitet och ordning (med undantag för inline-stilar som också använder !important). Det bör användas sparsamt, eftersom det kan göra koden svår att underhålla.

Syntax:

property: value !important;

Exempel 1: Överskrid en annan regel

p {
  color: blue !important;
}

p {
  color: red;
}

Alla <p>-element får blå text, även om den röda regeln står sist, eftersom !important vinner.

Exempel 2: Kombinera med inline-stil

<p style="color: green;">Denna text är grön.</p>
p {
  color: orange !important;
}

Texten blir orange, eftersom CSS-regeln med !important överskrider inline-stilen.

Obs:
Överanvänd inte !important. Det är bättre att strukturera CSS:en så att specificitet och ordning räcker för att styra vilka regler som gäller.

Sammanfattning

  • CSS används för att definiera utseendet på HTML-element.
  • Grundsyntaxen är selektor { egenskap: värde; }.
  • Det bästa och mest rekommenderade sättet att inkludera CSS är via en extern CSS-fil länkad med <link> i HTML:s <head>.
  • Intern CSS (<style>) och inline CSS (style-attribut) finns men bör användas sparsamt eller undvikas.
  • Kaskaden och specificitetsregler avgör vilken stil som appliceras om flera regler matchar samma element.

I nästa avsnitt dyker vi djupare ner i olika typer av selektorer.

Selektorer, färger och typografi i CSS

Att kunna välja ut och styla rätt HTML-element är grunden i CSS. I detta kapitel lär du dig använda selektorer (selectors), ange färger på olika sätt och styra textens utseende (typografi).

Motivation:
Med selektorer kan du styra exakt vilka delar av din webbsida som ska få särskild stil. Färg och typografi gör sidan mer läsbar och tilltalande.

Språkpolicy:
Svenska används i förklaringar, men engelska tekniska termer anges i parentes första gången de nämns. Variabel- och klassnamn skrivs på engelska.


CSS-selektorer: Att välja rätt element

Selektorer är mönster som matchar HTML-element. Här är de vanligaste:

1. Elementselektor (type selector)

  • Syntax: Elementets namn, t.ex. p, h1, div.
  • Vad den gör: Väljer alla element av den typen.
p {
  line-height: 1.6;
}
h2 {
  color: navy;
}

2. Klassselektor (class selector)

  • Syntax: Punkt (.) följt av klassnamnet, t.ex. .important.
  • Vad den gör: Väljer alla element med angiven klass. Ett element kan ha flera klasser.
<p class="important">Detta är viktigt.</p>
<span class="important highlight">Markerad text.</span>
<button class="button">Klicka här</button>
.important {
  font-weight: bold;
}
.highlight {
  background-color: yellow;
}
.button {
  padding: 10px 15px;
  border: 1px solid gray;
}

3. ID-selektor (ID selector)

  • Syntax: Hash (#) följt av ID-namnet, t.ex. #header.
  • Vad den gör: Väljer det enda element med angivet ID.
  • Viktigt: Ett ID ska vara unikt på sidan.
<header id="header">...</header>
#header {
  background-color: #f0f0f0;
  padding: 20px;
}

4. Attributselektor (attribute selector)

  • Syntax: Elementnamn följt av hakparenteser, t.ex. a[target="_blank"].
  • Vad den gör: Väljer element baserat på attribut och/eller värde.
a[target="_blank"] {
  color: green;
}
input[type="text"] {
  border: 1px solid #ccc;
}

5. Kombinatorer (combinators)

Kombinera selektorer för mer specifika regler:

  • Gruppering (grouping): Komma (,) för att ge flera selektorer samma stil.
    h1, h2, h3 {
      font-family: Georgia, serif;
    }
    
  • Selektor för ättling (descendant combinator): Mellanslag ( ) för att välja element som är ättlingar (barn, barnbarn, etc.) till ett annat element.
    nav ul {
      list-style: none;
    }
    
  • Barnselektor (child combinator): Större än-tecken (>) för att välja direkta barn.
    ul > li {
      margin-bottom: 5px;
    }
    

Specificitet:
ID-selektorer (#id) är starkare än klass- och attributselektorer (.class, [attr]), som är starkare än elementselektorer (p). När du kombinerar selektorer ökar specificiteten.


Färger i CSS

Du kan ange färger på flera sätt:

  • Namngivna färger (named colors): T.ex. red, blue, lightgray.
    body { background-color: lightgray; }
    
  • Hexadecimala koder (HEX): T.ex. #FF0000 (röd), #333 (mörkgrå).
    h1 { color: #3A8DDE; }
    
  • RGB/RGBA: T.ex. rgb(51, 51, 51), rgba(0, 0, 0, 0.5).
    p { color: rgb(51, 51, 51); }
    .overlay { background-color: rgba(0, 0, 0, 0.5); }
    
  • HSL/HSLA: T.ex. hsl(210, 100%, 50%).
    button { background-color: hsl(210, 100%, 50%); }
    

Grundläggande typografi

Typografi handlar om textens utseende. Några viktiga egenskaper:

  • font-family: Typsnitt. Ange gärna flera alternativ.
    body {
      font-family: "Helvetica Neue", Arial, sans-serif;
    }
    code {
      font-family: Consolas, Monaco, monospace;
    }
    
  • font-size: Textstorlek, t.ex. 16px, 2rem.
    p { font-size: 16px; }
    h1 { font-size: 2.5rem; }
    
  • font-weight: Tjocklek, t.ex. normal, bold, 300.
    strong { font-weight: bold; }
    .subtle { font-weight: 300; }
    
  • font-style: Stil, t.ex. normal, italic.
    em { font-style: italic; }
    
  • color: Textfärg.
    a { color: #007bff; }
    
  • text-align: Justering, t.ex. left, center.
    h1 { text-align: center; }
    
  • line-height: Radavstånd, t.ex. 1.6.
    p { line-height: 1.6; }
    
  • text-decoration: Dekoration, t.ex. understrykning.
    a { text-decoration: none; }
    a:hover { text-decoration: underline; }
    

Praktiska exempel

Exempel: Kombinera selektorer och färg

<ul>
  <li class="important">Viktigt</li>
  <li>Vanligt</li>
  <li class="highlight">Markerat</li>
</ul>
li {
  color: #333;
}
.important {
  color: red;
  font-weight: bold;
}
.highlight {
  background: yellow;
}

Exempel: Typografi och färg

<p class="info">Informationstext</p>
.info {
  font-family: Arial, sans-serif;
  font-size: 1.2rem;
  color: hsl(210, 100%, 40%);
  line-height: 1.5;
}

Sammanfattning

  • CSS-selektorer (element, klass, ID, attribut, kombinatorer) låter dig välja vilka HTML-element du vill styla.
  • Färger kan anges med namn, HEX, RGB(A) eller HSL(A).
  • Typografiska egenskaper styr textens utseende och läsbarhet.
  • Att behärska selektorer och grundläggande styling är fundamentalt för att skapa visuellt tilltalande webbsidor.

I nästa avsnitt tittar vi på box model, som beskriver hur utrymme hanteras runt HTML-element.

Box model och grundläggande layout i CSS

Att förstå CSS box model är avgörande för att kunna skapa tydliga och flexibla layouter på webbsidor. Box model beskriver hur varje HTML-element ritas upp som en rektangulär låda och hur dess storlek och avstånd till andra element beräknas.

Motivation:
Med kunskap om box model kan du styra exakt hur mycket plats ett element tar, hur det placeras och hur det samspelar med andra delar av sidan. Det är grunden för all layout i CSS.


Box model: delar

Varje låda i CSS består av fyra lager, utifrån och in:

  1. Margin (marginal):
    Yttersta lagret. Skapar ett genomskinligt utrymme utanför elementets kantlinje (border). Används för att skapa avstånd mellan olika element.

  2. Border (kantlinje):
    Ramen runt elementet. Har tjocklek, stil (t.ex. solid, dashed) och färg.

  3. Padding (utfyllnad):
    Genomskinligt utrymme innanför kantlinjen, men utanför innehållet. Skapar luft mellan kanten och innehållet.

  4. Content (innehåll):
    Själva innehållet i elementet (t.ex. text eller bild). Dimensionerna styrs av width och height (eller av innehållet om inget anges).


Visualisering

graph TD
    Margin -.-> Border -.-> Padding -.-> Content
    style Margin fill:#fff,stroke:#aaa,stroke-width:2px,stroke-dasharray: 5 5
    style Border fill:#fdd,stroke:#f00,stroke-width:2px
    style Padding fill:#dfd,stroke:#0a0,stroke-width:2px
    style Content fill:#ddf,stroke:#00f,stroke-width:2px

Diagram: Visualisering av CSS box model.


CSS-egenskaper för box model

  • width, height: Anger bredd och höjd för content-området.
  • padding: Luft innanför kantlinjen.
  • border: Själva ramen runt elementet.
  • margin: Luft utanför kantlinjen.

Exempel 1: Enkel box

.box {
  width: 200px;
  padding: 16px;
  border: 2px solid #333;
  margin: 24px;
  background-color: #eef;
}
<div class="box">
  Detta är en enkel box med padding, border och margin.
</div>

Exempel 2: Jämförelse mellan content-box och border-box

.box-content {
  width: 200px;
  padding: 20px;
  border: 4px solid #0077cc;
  margin: 16px;
  box-sizing: content-box;
  background: #d0f0ff;
}

.box-border {
  width: 200px;
  padding: 20px;
  border: 4px solid #cc7700;
  margin: 16px;
  box-sizing: border-box;
  background: #fff0d0;
}
<div class="box-content">
  box-sizing: content-box (standard)<br>
  Total bredd: 200px + 2×20px (padding) + 2×4px (border) = 248px
</div>
<div class="box-border">
  box-sizing: border-box<br>
  Total bredd: 200px (inklusive padding och border)
</div>

Exempel 3: Flera boxar med olika margin och padding

.box-small {
  width: 120px;
  padding: 8px;
  border: 2px dashed #888;
  margin: 8px;
  background: #f9f9f9;
}

.box-large {
  width: 240px;
  padding: 32px;
  border: 4px solid #444;
  margin: 32px;
  background: #e0e0e0;
}
<div class="box-small">
  Liten box med liten padding och margin.
</div>
<div class="box-large">
  Stor box med stor padding och margin.
</div>

Exempel 4: Ärftlighet och box model

Vissa egenskaper, som color och font-family, ärvs av barn-element. Andra, som margin och padding, ärvs inte.

.parent-box {
  color: darkblue;
  font-family: Verdana, sans-serif;
  border: 2px solid #222;
  padding: 12px;
  margin: 20px;
}
.child-box {
  border: 1px dotted #555;
  padding: 8px;
  margin: 10px;
}
<div class="parent-box">
  Förälder (parent-box)
  <div class="child-box">
    Barn (child-box) ärver färg och font, men har egna margin och padding.
  </div>
</div>

Analogier

Tänk dig box model som en flyttkartong:

  • Content: Själva sakerna i kartongen.
  • Padding: Bubbelplast runt sakerna.
  • Border: Själva kartongen.
  • Margin: Luft mellan kartongen och andra kartonger.

Sammanfattning

  • Box model styr hur HTML-element tar plats och placeras på sidan.
  • Den består av margin, border, padding och content.
  • Med rätt användning av box model kan du skapa flexibla och snygga layouter.
  • Använd gärna box-sizing: border-box; för enklare storleksberäkningar.

Blockelement och inline-element i CSS

Vad är blockelement?

Blockelement (block elements) är HTML-element som automatiskt tar upp hela bredden av sin förälder och börjar på en ny rad. Exempel på blockelement är <div>, <p>, <h1>, <ul>, och <li>. De används för att bygga sidans struktur.

Egenskaper för blockelement:

  • Börjar alltid på en ny rad.
  • Tar upp hela tillgängliga bredden.
  • Det går att ange width, height, margin och padding.

Exempel:

<div style="background: #e0e0ff; margin-bottom: 8px;">Detta är ett blockelement</div>
<p style="background: #ffe0e0;">Detta är också ett blockelement</p>

Vad är inline-element?

Inline-element (inline elements) är HTML-element som bara tar upp så mycket plats som behövs för sitt innehåll och ligger kvar på samma rad som andra element. Exempel på inline-element är <span>, <a>, <strong>, och <em>.

Egenskaper för inline-element:

  • Börjar inte på en ny rad.
  • Tar bara upp så mycket plats som behövs.
  • width och height har ingen effekt.
  • Endast horisontell padding och margin fungerar som väntat.

Exempel:

<p>
  Detta är <span style="background: #e0ffe0;">ett inline-element</span> mitt i en text.
</p>

Jämförelse: block vs inline

EgenskapBlockelementInline-element
Ny radJaNej
BreddHela föräldernsInnehållets bredd
width/heightJaNej
margin/paddingJaEndast horisontellt

Positionering av element med CSS

CSS erbjuder flera sätt att positionera element på en webbsida. Här är de vanligaste positioneringsmetoderna:

1. Static (standard)

Alla element har position: static; som standard. De placeras i sidans normala flöde.

.static-box {
  position: static;
}

2. Relative

Med position: relative; kan du flytta ett element i förhållande till dess ursprungliga plats med hjälp av top, right, bottom och left.

.relative-box {
  position: relative;
  left: 30px;
  top: 10px;
  background: #d0ffd0;
}

3. Absolute

position: absolute; placerar elementet i förhållande till närmaste förfader med position: relative; (eller till sidans kant om ingen sådan finns). Elementet tas bort från det normala flödet.

.parent {
  position: relative;
  width: 300px;
  height: 200px;
  background: #f0f0f0;
}
.child {
  position: absolute;
  top: 20px;
  left: 40px;
  background: #ffd0d0;
  padding: 8px;
}
<div class="parent">
  <div class="child">Absolut positionerad</div>
</div>

4. Fixed

Med position: fixed; placeras elementet i förhållande till webbläsarfönstret och stannar kvar även när du scrollar.

.fixed-box {
  position: fixed;
  bottom: 10px;
  right: 10px;
  background: #d0e0ff;
  padding: 10px;
}

5. Sticky

position: sticky; gör att elementet beter sig som relative tills du scrollar till en viss punkt, då blir det fixed.

.sticky-header {
  position: sticky;
  top: 0;
  background: #fffbe0;
  padding: 10px;
}

Illustration: CSS-positionering

flowchart TD
    A[Normalt sidflöde]
    B[Blockelement<br>(position: static)]
    C[Relativt flyttat<br>(position: relative;<br>top/left)]
    D[Absolut placerat<br>(position: absolute;<br>top/left)]
    E[Fast placerat<br>(position: fixed;<br>bottom/right)]
    F[Klistrigt<br>(position: sticky;<br>top: 0)]

    A --> B
    B -. Flyttas från ursprunglig plats .-> C
    A -. Tas ur sidflödet .-> D
    A -. Följer fönstret .-> E
    A -. Bete sig som static tills scroll .-> F

Sammanfattning

  • Blockelement bygger sidans grundstruktur och tar upp hela bredden.
  • Inline-element ligger kvar på samma rad och tar bara upp så mycket plats som behövs.
  • Med position kan du styra exakt var element hamnar på sidan.
  • Kombinera block/inline och positionering för att skapa flexibla och responsiva layouter.

Responsiv design med Media Queries

Idag surfar människor på webben från en mängd olika enheter: stora datorskärmar, bärbara datorer, surfplattor och mobiler av alla storlekar. En modern webbplats måste kunna anpassa sin layout och sitt innehåll för att se bra ut och vara användbar på alla dessa skärmar. Detta kallas responsiv web design (responsive web design, RWD).

Motivation:
Responsiv design gör att din webbplats fungerar och ser bra ut på alla enheter, vilket förbättrar användarupplevelsen och gör sidan mer tillgänglig.


Varför responsiv design?

  • Användarupplevelse: En sida som är designad för en stor skärm blir ofta svår att använda på en mobil (pyttesmå texter, knappar som är svåra att träffa, behov av att zooma och panorera). Responsiv design ger en optimerad upplevelse oavsett enhet.
  • SEO: Google prioriterar mobilvänliga webbplatser i sina sökresultat.
  • Underhåll: Istället för att bygga och underhålla separata webbplatser för dator och mobil (vilket var vanligt förr), har man en enda kodbas som anpassar sig.

Nyckelkomponenter i responsiv design

  1. Flexibel grid-baserad layout: Använd relativa enheter (som procent %, fr i CSS Grid eller Flexbox) istället för fasta pixelvärden för att skapa kolumner och layoutelement som kan ändra storlek.
  2. Flexibla bilder och media: Se till att bilder och andra mediafiler skalar ner proportionerligt för att passa mindre skärmar, t.ex. med max-width: 100%;.
  3. Media Queries: Använd CSS Media Queries för att applicera olika stilregler vid olika skärmstorlekar eller andra enhetsegenskaper.

Viewport meta-taggen

Det allra första steget för att göra en sida responsiv är att inkludera viewport meta-taggen i HTML-dokumentets <head>:

<meta name="viewport" content="width=device-width, initial-scale=1.0">
  • Vad den gör: Talar om för webbläsaren (särskilt på mobila enheter) hur den ska hantera sidans dimensioner och skalning.
    • width=device-width: Sätter sidans bredd till att vara densamma som enhetens skärmbredd.
    • initial-scale=1.0: Sätter den initiala zoomnivån till 100% (ingen zoom från start).
  • Utan denna tagg kommer mobila webbläsare ofta att försöka visa sidan som om den vore på en datorskärm och sedan förminska den, vilket leder till oläslig text och dålig användarupplevelse.

Media Queries: Anpassa stilar för olika skärmar

Media Queries är kärnan i hur vi applicerar olika CSS-regler baserat på enhetens egenskaper. Den vanligaste egenskapen att titta på är skärmens bredd.

  • Syntax: En Media Query skrivs med @media följt av en eller flera villkor inom parentes. CSS-reglerna som ska gälla endast när villkoret är uppfyllt placeras inom måsvingar ({ }).
/* Standardstilar (gäller alltid, ofta för mindre skärmar först – mobile-first design) */
body {
  font-size: 16px;
}
.container {
  width: 95%; /* Smalare på små skärmar */
  margin: 0 auto;
}

/* Stilar som appliceras ENDAST när skärmbredden är 768px ELLER MER */
@media (min-width: 768px) {
  body {
    font-size: 18px; /* Lite större text på större skärmar */
  }
  .container {
    width: 80%; /* Bredare container på större skärmar */
    max-width: 960px; /* Men inte bredare än 960px */
  }
  /* Fler regler för layout, t.ex. visa element sida vid sida */
}

/* Stilar som appliceras ENDAST när skärmbredden är 1200px ELLER MER */
@media (min-width: 1200px) {
  body {
    background-color: lightblue;
  }
  /* Ännu fler anpassningar för riktigt stora skärmar */
}
  • Vanliga villkor:

    • min-width: Applicerar reglerna när skärmbredden är minst det angivna värdet.
    • max-width: Applicerar reglerna när skärmbredden är högst det angivna värdet.
    • Man kan kombinera villkor med and, or, not.
    • Man kan också testa andra egenskaper som orientation (portrait/landscape), resolution, etc.
  • Breakpoints (brytpunkter): De specifika skärmbredder där layouten eller stilarna ändras (t.ex. 768px, 1200px i exemplet ovan) kallas breakpoints. Valet av breakpoints beror på designen och innehållet, inte nödvändigtvis på specifika enheters exakta dimensioner. Vanliga breakpoints kan vara runt t.ex. 600px, 768px, 992px, 1200px.


Relativa enheter

För att skapa flexibla och responsiva designer är det ofta bättre att använda relativa CSS-enheter istället för fasta (px):

  • % (procent): Relativt till förälderelementets motsvarande egenskap (t.ex. width: 50%; tar upp halva förälderns bredd).
  • em: Relativt till förälderelementets font-size.
  • rem (root em): Relativt till rot-elementets (<html>) font-size. Rekommenderas ofta för textstorlekar och ibland för padding/margin, eftersom det gör det enkelt att skala hela gränssnittet genom att bara ändra rot-fontstorleken (vilket också respekterar användarens webbläsarinställningar).
  • vw (viewport width): 1vw är 1% av webbläsarfönstrets bredd.
  • vh (viewport height): 1vh är 1% av webbläsarfönstrets höjd.

Genom att använda dessa enheter kan element och text anpassa sin storlek mer flytande när fönsterstorleken ändras.


Illustration: Media Queries och breakpoints

flowchart TD
    A[Bas-CSS för mobil]
    B{Breakpoint: min-width 768px}
    C{Breakpoint: min-width 1200px}
    D[Stilar för surfplatta/laptop]
    E[Stilar för desktop]

    A --> B
    B --> D
    D --> C
    C --> E

Sammanfattning

  • Responsiv design handlar om att skapa webbplatser som fungerar bra på alla enheter.
  • Detta uppnås genom flexibla layouter, flexibla bilder och framför allt Media Queries.
  • Glöm inte viewport meta-taggen i HTML:ens <head>.
  • Media Queries (@media) låter oss applicera olika CSS-regler vid specifika breakpoints (oftast baserat på min-width eller max-width).
  • Att använda relativa enheter som rem och % bidrar också till flexibiliteten.

I nästa avsnitt tittar vi specifikt på mobile-first design

Mobile-first design: Bygg för litet, skala uppåt

När vi utvecklar responsiva webbplatser finns det två huvudsakliga strategier för hur vi strukturerar vår CSS och våra Media Queries (mediefrågor):

  1. Desktop-first design: Man skriver först alla stilar för stora datorskärmar och använder sedan max-width i Media Queries för att modifiera eller ta bort stilar för mindre skärmar (surfplattor, mobiler).
  2. Mobile-first design: Man skriver först de grundläggande stilarna för de minsta skärmarna (mobiler) och använder sedan min-width i Media Queries för att lägga till eller modifiera stilar för successivt större skärmar.

Mobile-first design rekommenderas starkt och anses idag vara bästa praxis.

Mål: Förstå vad mobile-first design innebär, varför den är fördelaktig, och hur den implementeras med CSS och Media Queries.

Varför mobile-first design?

  • Fokus på kärninnehåll: Mobila skärmar har begränsat utrymme. Mobile-first design tvingar oss att prioritera det absolut viktigaste innehållet och funktionerna från början. Detta leder ofta till en renare och mer fokuserad användarupplevelse på alla enheter.
  • Prestanda: Mobila enheter har ofta långsammare processorer och sämre nätverksuppkoppling än datorer. Genom att ladda de enklaste stilarna först och bara lägga till mer komplexa layouter och funktioner för större skärmar (som har mer kraft), kan vi förbättra prestandan på mobilen.
  • Enklare CSS: Det är ofta lättare att lägga till stilar för större skärmar (t.ex. gå från en enkel kolumn till flera) än att skriva över eller nollställa komplexa desktop-stilar för mindre skärmar. Detta kan leda till mindre och mer lätthanterlig CSS.
  • Progressive enhancement: Mobile-first design går hand i hand med principen om progressiv förbättring (progressive enhancement). Man börjar med en grundläggande, fungerande upplevelse för alla (även äldre webbläsare eller enheter med begränsad funktionalitet) och lägger sedan på förbättringar (mer avancerad layout, JavaScript-effekter) för de webbläsare och enheter som stödjer dem.

Hur implementeras mobile-first design?

  1. Skriv bas-CSS för mobil: Skriv dina CSS-regler utanför några Media Queries. Dessa regler ska definiera grundutseendet och layouten för den minsta skärmen du siktar på (oftast en enkel, enkolumns-layout).

  2. Använd min-width Media Queries: Identifiera dina breakpoints (brytpunkter) där designen behöver anpassas för större skärmar.

  3. Lägg till stilar för större skärmar: Inuti @media (min-width: ...)-blocken, lägg till de CSS-regler som behövs för att anpassa layouten och utseendet för den större skärmstorleken. Du behöver bara skriva de regler som ändras eller läggs till – de grundläggande mobil-stilarna ärvs automatiskt.

Exempel (konceptuellt):

/* =================================== */
/* Bas-stilar (mobile-first design - gäller ALLTID) */
/* =================================== */
body {
  font-family: sans-serif;
  line-height: 1.5;
  padding: 10px;
}

.container {
  width: 100%; /* Tar upp hela bredden på små skärmar */
}

nav ul {
  padding: 0;
  list-style: none;
}

nav li {
  margin-bottom: 10px; /* Länkar under varandra */
}

.card {
  border: 1px solid #ccc;
  padding: 15px;
  margin-bottom: 15px;
}

/* Dölj sidopanelen på små skärmar */
.sidebar {
    display: none;
}

/* =================================== */
/* Mellanstora skärmar (t.ex. surfplattor och uppåt) */
/* =================================== */
@media (min-width: 768px) {
  body {
    padding: 20px;
  }

  .container {
    max-width: 960px; /* Begränsa bredden */
    margin: 0 auto; /* Centrera */
    display: flex; /* Använd Flexbox för layout */
    gap: 20px; /* Avstånd mellan flex-items */
  }

  main { /* Huvudinnehållet tar upp mer plats */
      flex: 3; /* Tar 3 delar av tillgängligt utrymme */
  }

  .sidebar { /* Visa sidopanelen */
      display: block;
      flex: 1; /* Tar 1 del av tillgängligt utrymme */
  }

  nav li {
    display: inline-block; /* Länkar bredvid varandra */
    margin-bottom: 0;
    margin-right: 15px;
  }
}

/* =================================== */
/* Stora skärmar (t.ex. desktop och uppåt) */
/* =================================== */
@media (min-width: 1200px) {
  body {
    font-size: 110%; /* Lite större text */
  }

  /* Eventuella ytterligare anpassningar för stora skärmar */
}

I det här exemplet:

  • Definieras grundläggande stilar för typsnitt, padding och en enkel layout (allt i en kolumn, ingen sidebar) först.
  • Vid 768px (min-width) introduceras Flexbox för att skapa en tvåkolumnslayout (main och sidebar), navigeringslänkarna placeras bredvid varandra, och containern centreras.
  • Vid 1200px (min-width) justeras textstorleken ytterligare.

Sammanfattning

Mobile-first design är en strategi där man designar och kodar för den minsta skärmen först och sedan använder min-width i Media Queries för att progressivt förbättra och anpassa layouten för större skärmar. Detta leder ofta till bättre prestanda på mobilen, renare kod och ett större fokus på kärninnehållet. Det är den rekommenderade metoden för att bygga responsiva webbplatser idag.

Nu är det dags att praktisera dessa CSS-koncept!

Praktiska övningar och projekt

Här får du hands-on-övningar för att träna på CSS:

  • Styling av formulär
  • Responsiva layouter
  • Navigation och menyer
  • Praktiska designutmaningar

Praktiska övningar: Grundläggande CSS

Nu är det dags att applicera det vi lärt oss om CSS! Vi fortsätter med "Om Mig"-sidan från kapitel 2 och ger den stil med CSS.

Mål:
Använda CSS för att:

  • Koppla en extern CSS-fil.
  • Använda olika selektorer (element, klass, ID).
  • Ändra färger och typografi.
  • Arbeta med box model (padding, margin, border).
  • Implementera grundläggande responsivitet med Media Queries (mobile-first design).

Förutsättningar:
Du har projektet om-mig-sida från kapitel 2, med index.html och Git initialiserat (och helst kopplat till GitHub).


Övning 1: Koppla CSS och grundläggande styling

  1. Skapa CSS-fil:
    I din om-mig-sida-mapp, skapa en ny fil som heter style.css.

  2. Länka från HTML:
    Öppna index.html. Inuti <head>, lägg till en <link>-tagg för att länka till din CSS-fil:

    <link rel="stylesheet" href="style.css">
    
  3. Grundläggande body-stilar (i style.css):

    • Sätt ett grundtypsnitt för hela sidan med font-family (t.ex. sans-serif).
    • Lägg till line-height för bättre läsbarhet (t.ex. 1.6).
    • Sätt en grundläggande textfärg (color) för body (t.ex. #333).
    • Lägg till paddingbody (t.ex. 20px) så att innehållet inte ligger klistrat mot kanterna på små skärmar.
    body {
      font-family: sans-serif;
      line-height: 1.6;
      color: #333;
      padding: 20px;
    }
    
  4. Styla rubriker:
    Ge h1 och h2 en annan färg.

    h1, h2 {
      color: darkcyan;
    }
    
  5. Commit:

    • git status (Du bör se style.css som ny och index.html som ändrad).
    • git add .
    • git commit -m "Lägg till CSS-fil och grundläggande body/h-styling"
  6. Visa:
    Öppna index.html i webbläsaren. Du bör nu se att typsnitt, radavstånd, färger och padding har ändrats!


Övning 2: Box model och selektorer

  1. Centrera innehåll (på större skärmar):
    Vi vill att innehållet ska vara centrerat och inte bli för brett på stora skärmar. Använd en container.

    • HTML: Omslut allt innehåll inuti <body> (utom eventuella <script>-taggar) med en <div> med klassen container. Om du använder <header>, <main>, <footer>, kan du omsluta dessa med containern eller lägga containern inuti <main>.
    • CSS: Lägg till regler för .container:
      .container {
        width: 100%; /* Mobile-first: full bredd */
        max-width: 800px; /* Maximal bredd på stora skärmar */
        margin-left: auto;
        margin-right: auto;
      }
      
      Tips: margin: 0 auto; är ett kortkommando för att centrera blockelement.
  2. Styla länkar:
    Ta bort understrykningen från länkar som standard och ändra färgen.

    a {
      color: dodgerblue;
      text-decoration: none;
    }
    
    a:hover {
      text-decoration: underline;
    }
    
  3. Styla bild (om du har en):
    Ge bilden en maximal bredd så att den inte blir större än sin container och lägg till lite marginal.

    img {
      max-width: 100%;
      height: auto;
      display: block;
      margin: 20px auto;
    }
    
  4. Commit:

    • git add .
    • git commit -m "Centrera innehåll, styla länkar och bild"
  5. Visa:
    Uppdatera webbläsaren. Se hur innehållet beter sig när du ändrar fönsterstorleken.


Övning 3: Responsivitet med Media Query

Låt oss göra så att bakgrundsfärgen på body ändras på lite större skärmar.

  1. Lägg till Media Query:
    Lägg till följande i slutet av style.css:

    @media (min-width: 768px) {
      body {
        background-color: #f0f8ff; /* AliceBlue */
        font-size: 18px;
      }
      /* Lägg till fler regler här som bara ska gälla på större skärmar */
    }
    
  2. Commit:

    • git add .
    • git commit -m "Gör sidan responsiv: ändra bakgrund och font på bredare skärm"
  3. Visa och testa:
    Öppna index.html. Ändra bredden på webbläsarfönstret. Ser du hur bakgrundsfärgen och textstorleken ändras när fönstret passerar 768 pixlars bredd?


Övning 4: Pusha till GitHub

Om du kopplade ditt repo till GitHub i kapitel 2, pusha dina ändringar:

  1. Kör git status för att se om du har några lokala commits som inte är pushade.
  2. Kör git pull (bra vana att göra innan push).
  3. Kör git push.
  4. Verifiera att dina ändringar (inklusive style.css) nu finns på GitHub.

Sammanfattning och nästa steg

Du har nu tagit dina första steg med CSS! Du har länkat en extern stilmall, använt olika selektorer, lagt till färger och typografi, arbetat med box model och introducerat responsivitet med en Media Query enligt mobile-first design. Du har också fortsatt att använda Git för att spara dina framsteg.

I nästa kapitel dyker vi ner i JavaScript för att lägga till interaktivitet på våra webbsidor.

Tekniska intervjufrågor: CSS och responsiv design

Detta avsnitt innehåller exempel på tekniska intervjufrågor som kan dyka upp gällande CSS och responsiv webbutveckling. Frågorna är utformade för att testa både teoretisk förståelse och praktisk kunskap.

Använd dessa frågor för att testa din kunskap och förbereda dig för tekniska intervjuer.


Fråga 1: CSS grundsyntax och inkludering

Fråga:
Förklara skillnaden mellan inline CSS, intern CSS och extern CSS. Vilken metod rekommenderas och varför?

Förslag till svar:

  • Inline CSS: style-attribut direkt på HTML-element, t.ex. <p style="color: red;">Text</p>.
  • Intern CSS: CSS i <style>-taggar i HTML:s <head>.
  • Extern CSS: Separat .css-fil länkad med <link rel="stylesheet" href="style.css">.

Extern CSS rekommenderas eftersom:

  • Separation: Håller struktur (HTML) och stil (CSS) åtskilda.
  • Underhåll: Enkelt att ändra hela webbplatsens utseende från en fil.
  • Prestanda: Webbläsaren kan cacha (spara) CSS-filen.
  • Återanvändbarhet: Samma stilar kan användas på flera sidor.

Inline CSS bör undvikas då det är svårt att underhålla och har högst specificitet.


Fråga 2: CSS-selektorer

Fråga:
Förklara skillnaden mellan dessa selektorer och ge exempel på när du skulle använda var och en:

p { }
.highlight { }
#header { }
nav ul li { }

Förslag till svar:

  • p – Elementselektor (type selector): Väljer alla <p>-element. Används för grundstilar på elementtyper.
  • .highlight – Klassselektor (class selector): Väljer alla element med class="highlight". Återanvändbar stil för flera element.
  • #header – ID-selektor (ID selector): Väljer elementet med id="header". Unikt per sida, högst specificitet.
  • nav ul li – Selektor för ättling (descendant combinator): Väljer <li>-element inuti <ul> som är inuti <nav>. Specifik kontextuell styling.

Bästa praxis: Använd klasser för återanvändbara stilar, ID:n sparsamt för unika element, och elementselektorer för grundstilar.


Fråga 3: Specificitet och kaskaden

Fråga:
Vilken färg får texten i detta exempel och varför?

<p id="viktigt" class="highlight">Text</p>
p { color: black; }
.highlight { color: blue; }
#viktigt { color: red; }
p.highlight { color: green; }

Förslag till svar:
Texten blir röd eftersom #viktigt (ID-selektor) har högst specificitet.

Specificitetsordning (högst till lägst):

  1. Inline-stilar (style-attribut)
  2. ID-selektorer (#viktigt)
  3. Klass-, attribut- och pseudoselektorer (.highlight, p.highlight)
  4. Elementselektorer (p)

p.highlight (kombinerad selektor) har högre specificitet än .highlight ensam, men lägre än #viktigt.


Fråga 4: Box model

Fråga:
Förklara CSS box model. Om ett element har width: 200px, padding: 20px, border: 5px solid black och margin: 10px, vad blir elementets totala bredd?

Förslag till svar: Box model består av (utifrån och in):

  1. Margin: Genomskinligt utrymme utanför elementet.
  2. Border: Elementets kantlinje.
  3. Padding: Genomskinligt utrymme mellan border och content.
  4. Content: Själva innehållet.

Standardberäkning (box-sizing: content-box):

  • Content: 200px
  • Padding: 20px × 2 = 40px
  • Border: 5px × 2 = 10px
  • Total bredd: 250px (margin räknas inte in i elementets bredd, men påverkar avståndet till andra element)

Med box-sizing: border-box: Padding och border inkluderas i width, så content-området blir 150px bred.


Fråga 5: Display-egenskaper

Fråga:
Vad är skillnaden mellan display: block, display: inline och display: inline-block?

Förslag till svar:

  • display: block:

    • Börjar på ny rad.
    • Tar hela tillgängliga bredden.
    • Respekterar width, height, alla margin och padding.
    • Exempel: <div>, <p>, <h1>.
  • display: inline:

    • Flödar med texten (ingen ny rad).
    • Tar bara nödvändig bredd.
    • Ignorerar width, height, margin-top/bottom, padding-top/bottom.
    • Exempel: <span>, <a>, <strong>.
  • display: inline-block:

    • Flödar som inline (ingen ny rad).
    • Men respekterar alla box model-egenskaper som block.
    • Perfekt för knappar eller element som ska vara bredvid varandra men ha specifika dimensioner.

Fråga 6: CSS-positionering

Fråga:
Förklara skillnaden mellan position: relative, position: absolute och position: fixed. Ge exempel på användningsfall för var och en.

Förslag till svar:

  • position: relative:

    • Elementet följer normala flödet.
    • Kan förskjutas med top, left etc. relativt till ursprunglig position.
    • Lämnar tomt utrymme där det skulle ha varit.
    • Användning: Små justeringar, som positioneringskontext för absoluta barn.
  • position: absolute:

    • Tas helt ur normala flödet.
    • Positioneras relativt till närmaste positionerade förälder.
    • Användning: Tooltips, dropdown-menyer, overlays.
  • position: fixed:

    • Positioneras relativt till webbläsarfönstret.
    • Stannar kvar vid scrollning.
    • Användning: Fasta navigationsmenyer, "tillbaka till toppen"-knappar.

Fråga 7: Färger och typografi

Fråga:
Förklara skillnaden mellan dessa färgformat och när du skulle använda dem:

color: red;
color: #FF0000;
color: rgb(255, 0, 0);
color: rgba(255, 0, 0, 0.5);

Förslag till svar:

  • red – Namngiven färg: Enkelt för grundfärger, begränsat utbud.
  • #FF0000 – HEX: Vanligast för webben, exakt färgkontroll, används av designers.
  • rgb(255, 0, 0) – RGB: Bra när man arbetar med RGB-värden från grafiska program.
  • rgba(255, 0, 0, 0.5) – RGBA: Som RGB men med genomskinlighet (alpha). Perfekt för overlays och genomskinliga element.

Rekommendation: HEX för fasta färger, RGBA när genomskinlighet behövs.


Fråga 8: Responsiv design – Media Queries

Fråga:
Vad är skillnaden mellan dessa två Media Queries och vilken strategi representerar de?

/* A */
@media (max-width: 768px) {
  .container { width: 100%; }
}

/* B */
@media (min-width: 768px) {
  .container { width: 80%; }
}

Förslag till svar:

  • A (max-width): Desktop-first design

    • Skriver stilar för stora skärmar först.
    • Ändrar/tar bort stilar för mindre skärmar.
    • Regel gäller när skärmen är 768px eller mindre.
  • B (min-width): Mobile-first design (rekommenderad)

    • Skriver grundstilar för mobil först.
    • Lägger till/förbättrar stilar för större skärmar.
    • Regel gäller när skärmen är 768px eller större.

Mobile-first design är bättre för prestanda och tvingar fram prioritering av kärninnehåll.


Fråga 9: Mobile-first design

Fråga:
Varför rekommenderas mobile-first design och hur implementerar du det i CSS?

Förslag till svar: Fördelar med mobile-first design:

  • Prestanda: Mobila enheter laddar enkla stilar först, komplexa läggs till progressivt.
  • Innehållsprioritet: Tvingar fokus på viktigaste innehållet först.
  • Enklare CSS: Lättare att lägga till än att ta bort/skriva över komplexa stilar.

Implementation:

/* Grundstilar för mobil (ingen media query) */
.container {
  width: 100%;
  padding: 10px;
}

/* Tablet och uppåt */
@media (min-width: 768px) {
  .container {
    width: 80%;
    padding: 20px;
    max-width: 1200px;
    margin: 0 auto;
  }
}

Viktigt: Alltid inkludera viewport meta-tag:

<meta name="viewport" content="width=device-width, initial-scale=1.0">

Fråga 10: Flexibla enheter

Fråga:
Förklara skillnaden mellan px, em, rem och %. När skulle du använda respektive enhet?

Förslag till svar:

  • px – Absolut enhet: Exakt storlek, alltid samma.

    • Använd för: Borders, små detaljer som ska vara konstanta.
  • em – Relativt till förälderns font-size.

    • Problem: Kan ge komplexa beräkningar vid nesting.
    • Använd för: Padding/margin som ska skala med textstorlek.
  • rem – Relativt till rot-elementets font-size.

    • Fördelar: Förutsägbar skalning, respekterar användarinställningar.
    • Använd för: Font-storlekar, padding, margin.
  • % – Relativt till förälderns motsvarande egenskap.

    • Använd för: Bredder i responsiva layouter.

Modern best practice: rem för typografi och spacing, % för layouter, px för borders.


Fråga 11: Flexbox layout

Fråga:
Förklara hur Flexbox fungerar. Vad är skillnaden mellan justify-content och align-items? Ge ett exempel på när du skulle använda Flexbox.

Förslag till svar: Flexbox (Flexible Box Layout):

  • Endimensionell layout – arbetar med antingen rader eller kolumner.
  • Flex container (förälder) och flex items (barn).
  • Aktiveras med display: flex; eller display: inline-flex;.

Viktiga egenskaper:

  • justify-content: Justerar items längs huvudaxeln (main axis).
    • Värden: flex-start, flex-end, center, space-between, space-around.
  • align-items: Justerar items längs tväraxeln (cross axis).
    • Värden: stretch, flex-start, flex-end, center, baseline.

Exempel – Centrera innehåll:

.container {
  display: flex;
  justify-content: center; /* Centrerar horisontellt */
  align-items: center;     /* Centrerar vertikalt */
  height: 100vh;
}

Användningsfall: Navigationsmenyer, centrering av innehåll, fördelning av utrymme mellan element.


Fråga 12: CSS Grid layout

Fråga:
Vad är skillnaden mellan CSS Grid och Flexbox? Visa hur du skulle skapa en enkel 3-kolumns layout med Grid.

Förslag till svar: CSS Grid vs Flexbox:

  • Grid: Tvådimensionell layout (rader och kolumner samtidigt).
  • Flexbox: Endimensionell layout (antingen rad eller kolumn).

Grid för komplexa layouter, Flexbox för enklare komponenter.

Exempel – 3-kolumns layout:

.container {
  display: grid;
  grid-template-columns: 1fr 2fr 1fr; /* 3 kolumner */
  gap: 20px; /* Avstånd mellan items */
}

/* Responsiv variant */
@media (max-width: 768px) {
  .container {
    grid-template-columns: 1fr; /* En kolumn på mobil */
  }
}

Grid-terminologi:

  • grid-template-columns/rows: Definierar kolumn/rad-storlekar.
  • fr (fraction): Tar en andel av tillgängligt utrymme.
  • gap: Avstånd mellan grid-items.
  • grid-area: Placerar items på specifika positioner.

Användningsfall: Sidlayouter, kort-layouts, komplexa responsiva designs.


Tips för tekniska intervjuer

  • Visa dina kunskaper visuellt – rita box model eller beskriv layout-flöde.
  • Förklara trade-offs – diskutera för- och nackdelar med olika tillvägagångssätt.
  • Ge praktiska exempel från projekt du arbetat på.
  • Fråga om kontext – olika lösningar passar olika situationer.
  • Visa problemlösningsförmåga – förklara hur du skulle felsöka CSS-problem.

Kapitel 4: JavaScript – Gör webben interaktiv

I de tidigare kapitlen har vi lärt oss att bygga upp webbsidor med HTML, ge dem struktur och semantik, samt styla dem med CSS. Men webben är mer än bara statiskt innehåll och utseende – det är interaktivitet, dynamik och möjligheten att reagera på användarens handlingar. Det är här JavaScript kommer in.

JavaScript är det språk som ger liv åt webbsidor. Med JavaScript kan du:

  • Hantera händelser som en användare initierar.
  • Ändra innehåll och stil på sidan utan att ladda om den.
  • Hämta och visa data från andra tjänster (API:er).
  • Skapa spel, animationer och mycket mer.

Motivation:
JavaScript är ett av världens mest använda programmeringsspråk och är oumbärligt för modern webbutveckling. Nästan alla interaktiva funktioner du ser på webben bygger på JavaScript.

Språkpolicy:
Svenska används i förklaringar, men engelska tekniska termer anges i parentes första gången de nämns. Variabel- och funktionsnamn skrivs på engelska.


Vad kommer du att lära dig i detta kapitel?

  • Introduktion till JavaScript: Vad är JavaScript och hur används det i webbläsaren?
  • Grunderna i programmering: Variabler, datatyper, operatorer och uttryck.
  • Villkor och logik: If-satser och jämförelser.
  • Loopar: Hur du upprepar kod med for- och while-loopar.
  • Funktioner: Hur du organiserar och återanvänder kod.
  • Interaktion med HTML (DOM): Hur JavaScript kan läsa och ändra innehåll på sidan.
  • Händelser: Hur du reagerar på användarens handlingar, t.ex. klick och tangenttryckningar.
  • Praktiska övningar: Du får skriva och testa JavaScript-kod direkt på dina egna webbsidor.

Nu är det dags att ta steget från statiska sidor

Introduktion till JavaScript

JavaScript är det programmeringsspråk som gör webbsidor interaktiva och dynamiska. Med JavaScript kan du reagera på användarens handlingar, ändra innehåll på sidan utan att ladda om den, skapa spel, animationer och mycket mer. Nästan alla moderna webbplatser använder JavaScript på något sätt.


Vad är JavaScript?

  • JavaScript är ett skriptspråk som körs i webbläsaren (client-side), men kan även köras på servrar (t.ex. med Node.js).
  • Det är ett av de tre grundläggande språken för webben:
    1. HTML – strukturen på sidan
    2. CSS – utseendet och layouten
    3. JavaScript – interaktivitet och logik

Varför använda JavaScript?

  • Interaktivitet: Gör det möjligt att reagera på klick, tangenttryckningar, formulär och andra händelser.
  • Dynamiskt innehåll: Ändra text, bilder och layout utan att ladda om sidan.
  • Validering: Kontrollera formulär innan de skickas till servern.
  • Animationer: Skapa rörelse och effekter.
  • Kommunikation: Hämta och skicka data till andra tjänster (API:er) utan att ladda om sidan.

Hur lägger man till JavaScript i en webbsida?

JavaScript kan inkluderas på två huvudsakliga sätt:

1. Inbäddat i HTML-filen

<!DOCTYPE html>
<html lang="sv">
<head>
  <meta charset="UTF-8">
  <title>Min första JS-sida</title>
</head>
<body>
  <h1>Hej!</h1>
  <script>
    console.log("Sidan är laddad!");
    alert("Välkommen till min sida!");
  </script>
</body>
</html>

2. Extern JavaScript-fil

Det rekommenderas att lägga JavaScript i en separat fil för bättre struktur och återanvändbarhet.

<!DOCTYPE html>
<html lang="sv">
<head>
  <meta charset="UTF-8">
  <title>Extern JS</title>
</head>
<body>
  <h1>Exempel</h1>
  <script src="script.js"></script>
</body>
</html>

I filen script.js:

console.log("Detta kommer från en extern fil!");

3. Importera JavaScript som modul

Du kan också inkludera JavaScript som en modul med attributet type="module". Det gör det möjligt att använda import och export för att dela kod mellan filer.

<script type="module" src="main.js"></script>

I filen main.js:

import { minFunktion } from './utils.js';

minFunktion();

Detta är användbart för större projekt där du vill strukturera din kod i flera filer.

Namngivning av JavaScript-filer

När du skapar JavaScript-filer är det viktigt att ge dem tydliga och beskrivande namn. Det gör det enklare att förstå vad filen innehåller och underlättar när projektet växer.

  • Korta filnamn:
    Används ofta för enklare eller generella skript.

    • script.js
    • main.js
    • app.js
  • Längre och mer beskrivande filnamn:
    För större projekt eller specifika funktioner är det vanligt att använda camelCase (små bokstäver och stor bokstav för varje nytt ord).

    • userProfile.js
    • dataFetcher.js
    • formValidator.js

Att använda beskrivande namn gör koden lättare att underhålla och förstå för både dig själv och andra utvecklare.


Grunder: Variabler och utskrift

Variabler

En variabel används för att spara data som kan användas senare.

let namn = "Anna";
let alder = 25;

Utskrift

  • Till konsolen:
    console.log("Hej!");
  • Som popup:
    alert("Varning!");

Sammanfattning

  • JavaScript gör webbsidor interaktiva och dynamiska.
  • Det kan läggas till direkt i HTML eller i en extern fil.
  • Med JavaScript kan du skapa allt från enkla effekter till avancerade webbapplikationer.
  • Nästa steg är att lära dig grunderna i programmering med JavaScript: variabler, datatyper, operatorer och logik.

Nu är du redo att börja skriva din första JavaScript

Variabler i JavaScript

Variabler är en grundläggande del av programmering. De används för att lagra och hantera data som kan ändras under programmets gång. I JavaScript kan du spara allt från siffror och text till objekt och listor i variabler.


Vad är en variabel?

En variabel är ett namn som pekar på ett värde i datorns minne. Du kan använda variabeln för att läsa, ändra eller använda värdet senare i programmet.


Namngivning av variabler

När du skapar variabler i JavaScript är det viktigt att använda tydliga och beskrivande namn. Det rekommenderas att skriva variabelnamn på engelska, eftersom det är standard inom programmering och gör koden lättare att förstå för andra utvecklare.

JavaScript använder vanligtvis camelCase för variabelnamn. Det innebär att det första ordet skrivs med små bokstäver och varje nytt ord börjar med stor bokstav, till exempel userName, totalAmount eller isActive.

Att vara konsekvent med namngivningen gör koden mer lättläst och underlättar samarbeten. Undvik att blanda språk eller olika stilar i samma projekt.

Exempel på bra variabelnamn:

let userEmail = "anna@example.com";
let itemCount = 5;
let isLoggedIn = false;

Exempel på mindre bra variabelnamn:

let användareNamn = "Anna"; // Blanda inte språk
let Item_count = 10;        // Undvik underscore och stor bokstav i början
let x = true;               // För kort och otydligt

Deklarera variabler

I modern JavaScript används oftast let och const för att skapa variabler. Äldre kod kan använda var, men det rekommenderas inte längre.

  • let – Skapar en variabel som kan ändras (reassignas).
  • const – Skapar en variabel som inte kan ändras (konstant). Värdet måste sättas direkt.

Exempel:

let name = "Anna";
let age = 25;

const pi = 3.14159;

Ändra värdet på en variabel

Variabler skapade med let kan få nya värden:

let score = 10;
score = 15; // score är nu 15

Variabler skapade med const kan inte ändras:

const maxUsers = 100;
// maxUsers = 200; // Fel! Går inte att ändra en const-variabel

Datatyper

Variabler kan innehålla olika typer av data:

  • String (text): "Hello", 'JavaScript'
  • Number (tal): 42, 3.14
  • Boolean (sant/falskt): true, false
  • Array (lista): [1, 2, 3]
  • Object (objekt): { name: "Anna", age: 25 }
  • Null (avsaknad av värde): null
  • Undefined (ej tilldelat värde): undefined

Exempel:

let text = "Hello!";
let number = 123;
let isActive = true;
let list = [1, 2, 3];
let person = { name: "Anna", age: 25 };
let emptyValue = null;
let unknown;

Namnge variabler

  • Variabelnamn får innehålla bokstäver, siffror, _ och $, men får inte börja med en siffra.
  • Använd beskrivande namn (t.ex. userName istället för x).
  • JavaScript är case sensitive: name och Name är olika variabler.

Utskrift och användning

Du kan använda variabler i uttryck och skriva ut dem med console.log:

let firstName = "Sara";
console.log("Hello, " + firstName + "!");

Sammanfattning

  • Variabler används för att lagra och hantera data i JavaScript.
  • Använd let för variabler som kan ändras, const för konstanter.
  • Variabler kan innehålla olika datatyper: text, tal, boolean, listor, objekt m.m.
  • Välj tydliga och beskrivande variabelnamn på engelska.
  • Variabler är grunden för att kunna skapa dynamiska och flexibla program.

Att förstå och använda variabler är ett av de första stegen mot att bli en skicklig programmerare

Funktioner i JavaScript

Funktioner är en av de viktigaste byggstenarna i JavaScript. De gör det möjligt att återanvända kod, strukturera programmet och dela upp logik i mindre, hanterbara delar.


Vad är en funktion?

En funktion är en namngiven kodblock som kan köras (anropas) flera gånger. Du kan skicka in värden (argument) till funktionen och få tillbaka ett resultat (return-värde).

Exempel:

function greet(name) {
  console.log("Hej, " + name + "!");
}

greet("Anna"); // Skriver ut: Hej, Anna!
greet("Erik"); // Skriver ut: Hej, Erik!

Skapa och anropa funktioner

Deklarera en funktion

function add(a, b) {
  return a + b;
}

Anropa en funktion

let sum = add(3, 5); // sum blir 8

Funktioner med och utan argument

  • Med argument:
    Funktioner kan ta emot värden (argument) som används inuti funktionen.

    function square(x) {
      return x * x;
    }
    let result = square(4); // result blir 16
    
  • Utan argument:
    Funktioner kan också deklareras utan argument.

    function sayHello() {
      console.log("Hej!");
    }
    sayHello();
    

Returnera värden

En funktion kan returnera ett värde med return. När return körs avslutas funktionen och värdet skickas tillbaka till den som anropade funktionen.

function multiply(a, b) {
  return a * b;
}
let produkt = multiply(2, 6); // produkt blir 12

Om inget return anges returnerar funktionen automatiskt undefined.


Funktioner som variabler (funktionella uttryck)

Du kan också spara en funktion i en variabel:

const subtract = function(a, b) {
  return a - b;
};
let diff = subtract(10, 3); // diff blir 7

Arrow functions (funktion utan nyckelordet function): () =>

Ett modernt och kortare sätt att skriva funktioner är med arrow functions:

const divide = (a, b) => {
  return a / b;
};
let kvot = divide(10, 2); // kvot blir 5

Om funktionen bara returnerar ett värde kan du skriva ännu kortare:

const double = x => x * 2;
let dubbelt = double(7); // dubbelt blir 14

Funktioner och scope

Variabler som deklareras inuti en funktion är lokala för den funktionen och kan inte nås utanför.

function testScope() {
  let lokal = "Jag finns bara här";
  console.log(lokal);
}
testScope();
// console.log(lokal); // Fel! lokal är inte definierad här

Funktioner som argument (callback functions)

Funktioner kan skickas som argument till andra funktioner. Detta är vanligt i t.ex. eventhantering och array-metoder.

function processArray(arr, callback) {
  for (let item of arr) {
    callback(item);
  }
}

processArray([1, 2, 3], function(num) {
  console.log(num * 2);
});
// Skriver ut: 2, 4, 6

Sammanfattning

  • Funktioner gör det möjligt att återanvända och strukturera kod.
  • Du kan skapa funktioner med function-syntax eller som arrow functions.
  • Funktioner kan ta emot argument och returnera värden.
  • Variabler inuti funktioner är lokala (scope).
  • Funktioner kan skickas som argument till andra funktioner (callback).

Att förstå och använda funktioner är avgörande för att skriva effektiv och läsbar JavaScript

Kontrollstrukturer i JavaScript

Kontrollstrukturer är byggstenarna som styr flödet i ett program. Med hjälp av dessa kan vi bestämma vilken kod som ska köras, när och hur många gånger. De vanligaste kontrollstrukturerna är villkorssatser (if/else) och loopar (for, while).


Villkorssatser (if, else if, else)

Med villkorssatser kan vi utföra olika kod beroende på om ett visst villkor är sant eller falskt.

Syntax:

if (villkor) {
  // kod som körs om villkoret är sant
} else if (annatVillkor) {
  // kod som körs om det andra villkoret är sant
} else {
  // kod som körs om inget av villkoren ovan är sant
}

Exempel:

let age = 18;

if (age >= 18) {
  console.log("Du är myndig.");
} else {
  console.log("Du är inte myndig.");
}

Jämförelseoperatorer

För att skapa villkor använder vi jämförelseoperatorer:

  • === lika med (värde och typ)
  • !== inte lika med (värde och typ)
  • == lika med (bara värde, undvik i modern JS)
  • != inte lika med (bara värde, undvik i modern JS)
  • > större än
  • < mindre än
  • >= större än eller lika med
  • <= mindre än eller lika med

Exempel:

let x = 5;
if (x !== 10) {
  console.log("x är inte 10");
}

Logiska operatorer

Kombinera flera villkor med logiska operatorer:

  • && och (båda villkoren måste vara sanna)
  • || eller (minst ett villkor måste vara sant)
  • ! inte (vänder på sant/falskt)

Exempel:

let temp = 20;
if (temp > 15 && temp < 25) {
  console.log("Lagom varmt!");
}

Switch-sats

När du har många möjliga värden att jämföra mot kan switch vara tydligare än många if/else.

Syntax:

switch (uttryck) {
  case värde1:
    // kod
    break;
  case värde2:
    // kod
    break;
  default:
    // kod om inget matchar
}

Exempel:

let day = "tisdag";
switch (day) {
  case "måndag":
    console.log("Ny vecka!");
    break;
  case "tisdag":
    console.log("Andra dagen.");
    break;
  default:
    console.log("Någon annan dag.");
}

Loopar

Loopar används för att upprepa kod flera gånger.

For-loop

Syntax:

for (start; villkor; steg) {
  // kod som upprepas
}

Exempel:

for (let i = 0; i < 5; i++) {
  console.log("i är nu: " + i);
}

While-loop

Syntax:

while (villkor) {
  // kod som upprepas så länge villkoret är sant
}

Exempel:

let count = 0;
while (count < 3) {
  console.log("Räknare: " + count);
  count++;
}

Break och continue

  • break – Avbryter loopen direkt.
  • continue – Hoppar över resten av koden i loopen och går till nästa varv.

Exempel:

for (let i = 0; i < 5; i++) {
  if (i === 3) break;      // Avbryter loopen när i är 3
  if (i === 1) continue;   // Hoppar över när i är 1
  console.log(i);
}
// Skriver ut: 0, 2

Sammanfattning

  • Kontrollstrukturer styr flödet i programmet.
  • Använd if, else if, else och switch för att fatta beslut.
  • Använd for och while för att upprepa kod.
  • Jämförelse- och logiska operatorer hjälper dig att skapa villkor.
  • break och continue ger extra kontroll i loopar.

Att behärska kontrollstrukturer är avgörande för att kunna skriva logisk och flexibel kod. I nästa avsnitt ska vi se hur JavaScript kan interagera med själva HTML-sidan.

Övningar

1. While-loop

Skriv en while-loop som räknar ner från 10 till 0 och skriver ut varje tal.

let count = 10;
// fortsätt med din lösning

2. Array-transformation

Skapa en array med temperaturer i Celsius och använd map för att konvertera dem till Fahrenheit. (Formel: F = C * 9/5 + 32)

// array med celsius temperaturer
let celsiusTemp = [0, 10, 20, 30, 40];
// fortsätt med din lösning

3. Filtrering med forEach

Använd forEach för att skriva ut alla jämna tal i en array.

// array siffror
let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// fortsätt med din lösning

Lösningsförslag

Övning 1

let count = 10;
while (count >= 0) {
    console.log(count);
    count--;
}
// Utskrift: 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0

Övning 2

let celsiusTemp = [0, 10, 20, 30, 40];
let fahrenheitTemp = celsiusTemp.map(temp => temp * 9/5 + 32);
console.log(fahrenheitTemp);
// Utskrift: [32, 50, 68, 86, 104]

Övning 3

let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
numbers.forEach(num => {
    if (num % 2 === 0) {
        console.log(num);
    }
});
// Utskrift: 2, 4, 6, 8, 10

DOM-manipulation och Events: Interagera med Sidan

JavaScript blir riktigt kraftfullt när vi använder det för att interagera med och ändra innehållet och strukturen på vår HTML-sida efter att den har laddats. Detta görs genom DOM (Document Object Model). Vi behöver också kunna reagera på användarens handlingar, vilket vi gör genom att hantera Events (Händelser).

Mål: Förstå vad DOM är, lära oss hur man väljer ut (selectar) HTML-element med JavaScript, hur man manipulerar (ändrar) deras innehåll, attribut och stilar, samt hur man kopplar händelselyssnare (addEventListener) för att reagera på användarinteraktioner som klick.


Vad är DOM (Document Object Model)?

När en webbläsare laddar en HTML-sida skapar den en modell av sidans struktur i minnet. Denna modell kallas DOM. DOM representerar HTML-dokumentet som ett träd av objekt (noder), där varje HTML-element (<body>, <h1>, <p>, <div>, etc.), textinnehåll och attribut blir ett objekt i trädet.

graph TD
    A["document"] --> B["html"];
    B --> C["head"];
    B --> D["body"];
    C --> E["title"];
    E --> F["Text: Min Sida"];
    D --> G["h1"];
    G --> H["Text: Rubrik"];
    D --> I["p"];
    I --> J["Text: Paragraf..."];

    style A fill:#ddd,stroke:#333,stroke-width:2px
    style F fill:#fff,stroke:#ccc,stroke-width:1px
    style H fill:#fff,stroke:#ccc,stroke-width:1px
    style J fill:#fff,stroke:#ccc,stroke-width:1px

Diagram: Förenklat exempel på DOM-trädet för en enkel HTML-sida.

Varför är DOM viktigt?
JavaScript kan komma åt och manipulera detta DOM-träd. Det betyder att vi med JavaScript kan:

  • Hitta specifika HTML-element.
  • Ändra deras textinnehåll.
  • Ändra deras attribut (som src på en bild eller href på en länk).
  • Ändra deras CSS-stilar.
  • Lägga till nya element.
  • Ta bort befintliga element.

Allt detta kan göras utan att behöva ladda om sidan. Det är grunden för dynamiska webbapplikationer.


Välja element från DOM

För att kunna manipulera ett element måste vi först "hitta" det i DOM-trädet. Detta görs med olika metoder på det globala document-objektet:

Moderna metoder (rekommenderas)

  • document.querySelector(cssSelektor)
    Returnerar det första elementet i dokumentet som matchar den angivna CSS-selektorn (t.ex. #minId, .minKlass, p, nav > ul > li).
    Om ingen matchning hittas returneras null.
    Exempel:

    const mainHeading = document.querySelector('#main-title');
    const firstButton = document.querySelector('.btn');
    const navLink = document.querySelector('nav ul li a');
    
  • document.querySelectorAll(cssSelektor)
    Returnerar en NodeList (en samling som liknar en array) med alla element som matchar selektorn.
    Om inga matchningar hittas returneras en tom NodeList.
    Exempel:

    const allParagraphs = document.querySelectorAll('p');
    const allButtons = document.querySelectorAll('.btn');
    
    allButtons.forEach(button => {
      console.log("Hittade en knapp!");
      // Gör något med varje knapp här
    });
    

Äldre metoder (fortfarande vanliga)

  • document.getElementById(id) – Returnerar elementet med det angivna ID:t (eller null).
  • document.getElementsByClassName(klassnamn) – Returnerar en HTMLCollection (en live-samling) med alla element som har den angivna klassen.
  • document.getElementsByTagName(taggnamn) – Returnerar en HTMLCollection med alla element av den angivna taggtypen (t.ex. 'p', 'div').

Skillnad NodeList vs HTMLCollection:
En NodeList (från querySelectorAll) är oftast statisk, medan en HTMLCollection (från getElementsByClassName/TagName) är live och uppdateras automatiskt om DOM ändras. NodeList har också forEach-metoden inbyggd, vilket HTMLCollection ofta saknar.


Manipulera element

När du väl har valt ett eller flera element och sparat dem i en variabel, kan du ändra dem:

Ändra textinnehåll

  • element.textContent – Hämtar eller sätter textinnehållet för ett element och dess ättlingar, utan HTML-taggar.
  • element.innerText – Liknar textContent, men tar hänsyn till CSS-styling och inkluderar inte text från dolda element.
const heading = document.querySelector('h1');
console.log(heading.textContent); // Visar nuvarande text
heading.textContent = "Ny Rubrik från JS!"; // Ändrar texten

Ändra HTML-innehåll

  • element.innerHTML – Hämtar eller sätter HTML-koden inuti ett element.
    OBS: Använd med försiktighet, speciellt med osäker data (risk för XSS).
const contentDiv = document.querySelector('#content');
// contentDiv.innerHTML = "<h2>Nytt innehåll</h2><p>Detta är en paragraf.</p>";

Ändra attribut

  • element.setAttribute('attributnamn', 'nyttVärde') – Sätter värdet på ett attribut.
  • element.getAttribute('attributnamn') – Hämtar värdet på ett attribut.
  • För vanliga attribut som id, src, href, class kan man ofta komma åt dem direkt som egenskaper.
const link = document.querySelector('a');
link.href = "https://www.google.com";
link.target = "_blank";

const image = document.querySelector('img');
image.setAttribute('src', 'nybild.jpg');
image.alt = "Beskrivning av ny bild";

Ändra CSS-stilar

  • element.style.cssEgenskap = 'värde' – Ändrar inline-stilen för ett element. CSS-egenskaper skrivs i camelCase (t.ex. backgroundColor).
    const box = document.querySelector('.box');
    box.style.backgroundColor = 'lightblue';
    box.style.padding = '20px';
    box.style.border = '1px solid blue';
    
  • Bättre metod: Ändra elementets klass med element.classList:
    • element.classList.add('ny-klass')
    • element.classList.remove('gammal-klass')
    • element.classList.toggle('aktiv-klass')
    • element.classList.contains('viss-klass')
/* I style.css */
.highlight {
  background-color: yellow;
  font-weight: bold;
}
const importantText = document.querySelector('.important');
importantText.classList.add('highlight');

Skapa och lägga till element

  • document.createElement('taggnamn') – Skapar ett nytt HTML-element.
  • parentElement.appendChild(newElement) – Lägger till newElement som sista barnet till parentElement.
  • parentElement.insertBefore(newElement, referenceElement) – Infogar newElement före referenceElement.
  • element.remove() – Tar bort elementet från DOM.
const newItem = document.createElement('li');
newItem.textContent = "Ny punkt";
const list = document.querySelector('#myList');
list.appendChild(newItem);

Hantera händelser

Events är händelser som inträffar i webbläsaren, oftast initierade av användaren (klick, tangenttryck, musrörelser) men också av webbläsaren själv (t.ex. att sidan laddats klart).

JavaScript låter oss "lyssna" efter dessa händelser och köra en funktion (en event handler eller callback function) när händelsen inträffar.

Koppla händelselyssnare

  • element.addEventListener(eventType, eventHandlerFunction)
    Den moderna och rekommenderade metoden för att koppla en händelselyssnare.
const myButton = document.querySelector('#myButton');

function handleButtonClick(event) {
  console.log("Knappen klickades!");
  event.target.textContent = "Klickad!";
  event.target.style.backgroundColor = 'lightgreen';
}

myButton.addEventListener('click', handleButtonClick);

const anotherButton = document.querySelector('#otherBtn');
anotherButton.addEventListener('click', (event) => {
  event.target.classList.toggle('active');
});

Vanliga händelsetyper

  • Mus-händelser: click, dblclick, mouseover, mouseout, mousedown, mouseup, mousemove
  • Tangentbords-händelser: keydown, keyup, keypress
  • Formulär-händelser: submit, input, change, focus, blur
  • Fönster/Dokument-händelser: load, DOMContentLoaded, resize, scroll

Förhindra standardbeteende

  • event.preventDefault() – Stoppar webbläsarens standardbeteende för händelsen.
    Används ofta för att hantera formulär med JavaScript.
const myForm = document.querySelector('#myForm');
myForm.addEventListener('submit', (event) => {
  event.preventDefault();
  console.log("Formulär skulle ha skickats, men vi stoppade det!");
  // Hantera formulärdata här...
});

Sammanfattning

DOM är webbläsarens representation av HTML-sidan som ett träd av objekt. JavaScript kan interagera med DOM för att:

  • Välja element: Med querySelector (första matchningen) och querySelectorAll (alla matchningar) baserat på CSS-selektorer.
  • Manipulera element: Ändra textContent, innerHTML (försiktigt!), attribut (setAttribute, direkta egenskaper), stilar (element.style eller, bättre, element.classList).
  • Skapa och ta bort element: Med createElement, appendChild, remove.
  • Reagera på events: Med addEventListener kan du köra JavaScript-kod när användaren interagerar med sidan.

Att behärska DOM-manipulation och händelsehantering är centralt för frontend-utveckling med JavaScript.

Praktiska övningar: JavaScript och interaktivitet

Att läsa teori är viktigt, men det är genom att skriva och testa kod som du verkligen lär dig JavaScript. Här hittar du övningar som hjälper dig att befästa kunskaperna från kapitlet om JavaScript, DOM-manipulation och händelser.

Mål:
Träna på att använda JavaScript för att skapa interaktivitet, manipulera DOM och hantera händelser på webbsidor.

Förutsättningar:
Du har en HTML-fil att arbeta med och kan lägga till eller länka en JavaScript-fil. Du kan öppna sidan i en webbläsare och se resultatet av din kod.


Övning 1: Skriv ut meddelande i konsolen

  1. Skapa en fil script.js och koppla den till din HTML-fil med <script src="script.js"></script>.
  2. Skriv kod som skriver ut "Hello, JavaScript!" i webbläsarens konsol.
  3. Öppna webbsidan och kontrollera i konsolen (F12) att meddelandet syns.

Övning 2: Ändra text på sidan med JavaScript

  1. Lägg till ett element i din HTML, t.ex. <p id="message">Detta ska ändras</p>.
  2. Skriv JavaScript som ändrar texten i paragrafen till "Texten är nu ändrad!" när sidan laddas.

Övning 3: Byt färg på ett element vid knapptryck

  1. Lägg till en knapp: <button id="colorBtn">Byt färg</button> och ett text-element: <p id="colorText">Färgtest</p>.
  2. Skriv JavaScript som gör att texten i colorText byter färg (t.ex. till blå) när du klickar på knappen.

Övning 4: Lägg till nya element i DOM

  1. Lägg till en tom lista i HTML: <ul id="myList"></ul> och en knapp: <button id="addBtn">Lägg till punkt</button>.
  2. Skriv JavaScript som lägger till ett nytt <li>-element med valfri text i listan varje gång du klickar på knappen.

Övning 5: Enkel räknare

  1. Lägg till en knapp: <button id="countBtn">Räkna</button> och ett element för att visa räknaren: <span id="counter">0</span>.
  2. Skriv JavaScript som ökar värdet i counter med 1 varje gång du klickar på knappen.

Övning 6: Formulär och validering

  1. Lägg till ett enkelt formulär i HTML:
    <form id="myForm">
      <input type="text" id="nameInput" placeholder="Skriv ditt namn">
      <button type="submit">Skicka</button>
    </form>
    <p id="formMessage"></p>
    
  2. Skriv JavaScript som:
    • Förhindrar att sidan laddas om när formuläret skickas.
    • Läser av värdet i nameInput.
    • Skriver ut ett meddelande i formMessage, t.ex. "Hej, [namn]!", om fältet inte är tomt. Om det är tomt, visa ett felmeddelande.

Sammanfattning och nästa steg

Genom att göra dessa övningar får du praktisk erfarenhet av att använda JavaScript för att manipulera DOM och hantera händelser. Fortsätt experimentera – prova att lägga till fler funktioner, kombinera övningarna eller skapa egna små projekt!

I nästa kapitel lär du dig mer om hur du strukturerar och återanvänder kod med funktioner och moduler, samt hur du arbetar med mer avancerade datatyper och asynkrona operationer.

Tekniska Intervjufrågor: JavaScript och DOM-manipulation

Detta avsnitt innehåller exempel på tekniska intervjufrågor som kan dyka upp gällande JavaScript och frontend-programmering. Frågorna är utformade för att testa både teoretisk förståelse och praktisk kunskap.

Använd dessa frågor för att testa din kunskap och förbereda dig för tekniska intervjuer.


Fråga 1: JavaScript Grundläggande och Inkludering

Fråga: "Förklara skillnaden mellan att inkludera JavaScript inline, internt och externt. Varför placeras <script>-taggar oftast före </body>?"

Förslag till svar:

  • Inline: onclick="alert('Klick')" - Bör undvikas, blandar struktur och beteende
  • Internt: <script>-taggar i HTML-dokumentet - OK för små tester
  • Externt: Separat .js-fil länkad med <script src="script.js"> - Rekommenderas

Varför före </body>:

  • DOM-laddning: HTML-element måste finnas innan JavaScript kan manipulera dem
  • Prestanda: Sidan kan börja renderas innan script laddas
  • Användarupplevelse: Undviker att script blockerar sidladdningen

Best practice: Extern JS-fil precis före </body> för separation av concerns och bättre underhållbarhet.


Fråga 2: Variabler och Deklarationer

Fråga: "Vad är skillnaden mellan let, const och var? När skulle du använda respektive?"

Förslag till svar:

  • const: Konstant värde, kan inte tilldelas om, block-scope
    const name = "Alice"; // Kan inte ändras
    
  • let: Variabelt värde, kan tilldelas om, block-scope
    let age = 25;
    age = 26; // OK
    
  • var: Äldre syntax, function-scope (inte block-scope), hoisting-problem

Best practice:

  1. Använd const som standard - signalerar att värdet inte ska ändras
  2. Använd let när du behöver ändra värdet
  3. Undvik var - orsakar förvirring med scope

Block-scope exempel:

if (true) {
  let blockVar = "synlig här";
  const blockConst = "också synlig här";
}
// blockVar och blockConst är INTE synliga här

Fråga 3: Datatyper och Typomvandling

Fråga: "Förklara JavaScripts primitiva datatyper. Vad händer i följande kod och varför?"

console.log("5" + 3);
console.log("5" - 3);
console.log(true + 1);

Förslag till svar: Primitiva datatyper:

  • string - text: "Hej"
  • number - tal: 42, 3.14
  • boolean - sant/falskt: true, false
  • undefined - ej tilldelat värde
  • null - avsiktligt tomt värde
  • symbol - unik identifierare
  • bigint - mycket stora heltal

Typomvandling (Type Coercion):

console.log("5" + 3);    // "53" - + konkatenerar strängar
console.log("5" - 3);    // 2 - konverterar "5" till number
console.log(true + 1);   // 2 - true blir 1

Varför: JavaScript försöker automatiskt konvertera typer, vilket kan ge oväntade resultat. Var försiktig med operatorer på olika typer.


Fråga 4: Funktioner och Syntax

Fråga: "Visa tre olika sätt att skriva funktioner i JavaScript. Vad är skillnaderna mellan dem?"

Förslag till svar:

// 1. Funktionsdeklaration
function add(a, b) {
  return a + b;
}

// 2. Funktionsuttryck
const multiply = function(a, b) {
  return a * b;
};

// 3. Arrow function (ES6)
const divide = (a, b) => a / b;

// Kort arrow function (en parameter)
const square = x => x * x;

Skillnader:

  • Deklaration: Hoistas (kan anropas före deklaration)
  • Uttryck: Hoistas inte, måste deklareras före anrop
  • Arrow: Kortare syntax, annorlunda this-binding, hoistas inte

När använda vad:

  • Arrow functions: För korta, enkla funktioner och callbacks
  • Deklarationer: För huvudfunktioner i programmet
  • Uttryck: När du vill tilldela funktion till variabel

Fråga 5: Scope och Synlighet

Fråga: "Förklara skillnaden mellan globalt, lokalt och block-scope. Vad skrivs ut i denna kod?"

const global = "Jag är global";

function testScope() {
  const local = "Jag är lokal";
  
  if (true) {
    const block = "Jag är i block";
    console.log(global); // ?
    console.log(local);  // ?
    console.log(block);  // ?
  }
  
  console.log(block); // ?
}

Förslag till svar: Scope-typer:

  • Global scope: Tillgänglig överallt i programmet
  • Funktions-scope (lokalt): Tillgänglig endast inuti funktionen
  • Block-scope: Tillgänglig endast inuti {} (för let/const)

Utskrift:

console.log(global); // "Jag är global" - global variabel
console.log(local);  // "Jag är lokal" - lokal till funktionen  
console.log(block);  // "Jag är i block" - i samma block

console.log(block);  // ReferenceError! block finns inte här

Princip: Inre scope kan komma åt yttre scope, men inte tvärtom.


Fråga 6: Kontrollstrukturer

Fråga: "Skriv kod som kontrollerar en användares ålder och ger olika meddelanden. Använd både if/else och switch för att visa skillnaden."

Förslag till svar:

// Med if/else (för ranges/intervall)
function checkAgeIf(age) {
  if (age < 13) {
    return "Barn";
  } else if (age < 20) {
    return "Tonåring";
  } else if (age < 65) {
    return "Vuxen";
  } else {
    return "Senior";
  }
}

// Med switch (för specifika värden)
function checkDayType(dayNumber) {
  switch (dayNumber) {
    case 1:
    case 2:
    case 3:
    case 4:
    case 5:
      return "Vardag";
    case 6:
    case 7:
      return "Helg";
    default:
      return "Ogiltig dag";
  }
}

När använda vad:

  • if/else: För intervall, komplexa villkor, booleans
  • switch: För specifika värden, många exakta fall
  • Viktigt: Glöm inte break; i switch-case (om du inte returnar)!

Fråga 7: Loopar och Iteration

Fråga: "Visa olika sätt att iterera över en array. Vad är för- och nackdelarna med varje metod?"

Förslag till svar:

const fruits = ["äpple", "banan", "apelsin"];

// 1. Traditional for-loop
for (let i = 0; i < fruits.length; i++) {
  console.log(i, fruits[i]);
}
// Fördelar: Full kontroll över index, kan ändra loop-variabel
// Nackdelar: Mer kod, risk för off-by-one errors

// 2. for...of (ES6)
for (const fruit of fruits) {
  console.log(fruit);
}
// Fördelar: Ren syntax, direkt tillgång till värden
// Nackdelar: Ingen direkt tillgång till index

// 3. forEach (array method)
fruits.forEach((fruit, index) => {
  console.log(index, fruit);
});
// Fördelar: Funktionell stil, index som parameter
// Nackdelar: Kan inte break/continue

// 4. while loop
let i = 0;
while (i < fruits.length) {
  console.log(fruits[i]);
  i++;
}
// Fördelar: Flexibel, bra för okänt antal iterationer
// Nackdelar: Risk för oändlig loop om man glömmer öka i

Fråga 8: DOM och Element-selektion

Fråga: "Vad är DOM? Visa olika sätt att välja element från DOM och när du skulle använda varje metod."

Förslag till svar: DOM (Document Object Model):

  • Webbläsarens representation av HTML-dokumentet som ett träd av objekt
  • JavaScript kan manipulera detta träd för att ändra sidan dynamiskt
  • Varje HTML-element blir ett objekt med egenskaper och metoder

Element-selektion:

// Modern metod (rekommenderas)
const element = document.querySelector('#myId');
const elements = document.querySelectorAll('.myClass');

// Äldre metoder (fortfarande används)
const byId = document.getElementById('myId');
const byClass = document.getElementsByClassName('myClass');
const byTag = document.getElementsByTagName('p');

Skillnader:

  • querySelector: Första matchning, CSS-selektor syntax
  • querySelectorAll: Alla matchningar, returnerar NodeList
  • getElementById: Snabbast för ID, bara ett element
  • getElementsByClass/Tag: Returnerar HTMLCollection (live)

Best practice: Använd querySelector/querySelectorAll för flexibilitet.


Fråga 9: DOM-manipulation

Fråga: "Hur ändrar du innehållet, attribut och stilar på ett HTML-element med JavaScript? Visa exempel."

Förslag till svar:

const element = document.querySelector('#myElement');

// Ändra textinnehåll
element.textContent = "Ny text";          // Säker, bara text
element.innerHTML = "<strong>HTML</strong>"; // Osäker med user input!

// Ändra attribut
element.setAttribute('src', 'newimage.jpg');
element.src = 'newimage.jpg';             // Direktegenskap
const currentSrc = element.getAttribute('src');

// Ändra stilar (undvik för många ändringar)
element.style.backgroundColor = 'red';
element.style.fontSize = '20px';

// Bättre: Använd CSS-klasser
element.classList.add('highlight');       // Lägg till klass
element.classList.remove('old-style');   // Ta bort klass  
element.classList.toggle('active');      // Växla klass
const hasClass = element.classList.contains('highlight');

// Skapa och lägga till element
const newDiv = document.createElement('div');
newDiv.textContent = "Nytt element";
document.body.appendChild(newDiv);

Best practice: Använd classList för stiländringar istället för style.


Fråga 10: Event Handling

Fråga: "Förklara hur händelsehantering fungerar i JavaScript. Skriv kod som hanterar en knappklickning och visa vad event-objektet innehåller."

Förslag till svar:

// HTML: <button id="myButton">Klicka mig</button>

const button = document.querySelector('#myButton');

// Modern metod (rekommenderas)
button.addEventListener('click', handleClick);

function handleClick(event) {
  console.log('Knapp klickad!');
  
  // Event-objektet innehåller:
  console.log('Event type:', event.type);        // 'click'
  console.log('Target element:', event.target);  // Knappen som klickades
  console.log('Mouse position:', event.clientX, event.clientY);
  
  // Förhindra standardbeteende (t.ex. för formulär)
  event.preventDefault();
  
  // Ändra knappens utseende
  event.target.textContent = 'Klickad!';
  event.target.classList.add('clicked');
}

// Alternativ: arrow function direkt
button.addEventListener('click', (event) => {
  console.log('Klick med arrow function!');
});

Vanliga events: click, submit, input, mouseover, keydown, load

Viktigt: Använd addEventListener istället för onclick-attribut för separation of concerns.


Fråga 11: Formulärhantering och Validering

Fråga: "Hur hanterar du formulärinskickning med JavaScript? Visa hur du förhindrar standardbeteendet och validerar input."

Förslag till svar:

// HTML: 
<form id="myForm">
   <input type="email" id="email" required>
   <button type="submit">Skicka</button>
</form>

const form = document.querySelector('#myForm');
const emailInput = document.querySelector('#email');

form.addEventListener('submit', handleSubmit);

function handleSubmit(event) {
  event.preventDefault(); // Stoppa normal formulärinskickning
  
  // Hämta värden
  const email = emailInput.value.trim();
  
  // Enkel validering
  if (!email) {
    showError('E-post krävs');
    return;
  }
  
  if (!isValidEmail(email)) {
    showError('Ogiltig e-postadress');
    return;
  }
  
  // Skicka data (t.ex. till server)
  console.log('Skickar:', { email });
  showSuccess('Formulär skickat!');
}

function isValidEmail(email) {
  return email.includes('@') && email.includes('.');
}

function showError(message) {
  console.error(message);
  // Visa felmeddelande i UI
}

function showSuccess(message) {
  console.log(message);
  // Visa framgångsmeddelande i UI
}

Viktigt: Alltid validera på server också - klientvalidering är bara för användarupplevelse.


Fråga 12: Felsökning och Debug-tekniker

Fråga: "Vilka verktyg och tekniker använder du för att felsöka JavaScript-kod? Visa praktiska exempel."

Förslag till svar: Felsökningsverktyg:

  1. Console.log() - Grundläggande utskrifter
const data = { name: "Alice", age: 25 };
console.log('Data:', data);
console.log('Endast namnet:', data.name);

// Andra console-metoder
console.error('Detta är ett fel');
console.warn('Detta är en varning');
console.table(data); // Visa objekt som tabell
  1. Browser Developer Tools
// Sätt breakpoints i koden
function calculateTotal(price, tax) {
  debugger; // Pausar exekvering här
  const total = price + (price * tax);
  return total;
}
  1. Defensive Programming
function processUser(user) {
  // Kontrollera input
  if (!user) {
    console.error('User is null or undefined');
    return;
  }
  
  if (typeof user.age !== 'number') {
    console.warn('User age is not a number:', user.age);
  }
  
  // Fortsätt med logik...
}
  1. Try-Catch för felhantering
try {
  const result = riskyOperation();
  console.log('Success:', result);
} catch (error) {
  console.error('Something went wrong:', error.message);
}

Best practices: Använd console.log strategiskt, lär dig utvecklarverktygen, skriv defensiv kod.


Tips för Tekniska Intervjuer

  • Skriv kod på whiteboard/papper - träna på att skriva JavaScript utan editor
  • Förklara din tankeprocess - prata igenom vad du gör steg för steg
  • Testa din kod mentalt - gå igenom koden rad för rad
  • Fråga om krav - klargör vad som förväntas innan du börjar koda
  • Börja enkelt - löss grundproblemet först, lägg till features sedan
  • Hantera edge cases - diskutera vad som händer med oväntad input

Tekniska intervjufrågor: CSS och responsiv design

Detta avsnitt innehåller exempel på tekniska intervjufrågor som kan dyka upp gällande CSS och responsiv webbutveckling. Frågorna är utformade för att testa både teoretisk förståelse och praktisk kunskap.

Använd dessa frågor för att testa din kunskap och förbereda dig för tekniska intervjuer.


Fråga 1: CSS grundsyntax och inkludering

Fråga:
Förklara skillnaden mellan inline CSS, intern CSS och extern CSS. Vilken metod rekommenderas och varför?

Svar:

  • Inline CSS: Skrivs direkt i HTML-elementets style-attribut, t.ex. <p style="color: red;">Text</p>.
  • Intern CSS: Skrivs i en <style>-tagg i HTML-dokumentets <head>.
  • Extern CSS: Skrivs i en separat .css-fil som länkas in med <link rel="stylesheet" href="style.css">.

Extern CSS rekommenderas eftersom det separerar struktur och utseende, gör det lättare att återanvända och underhålla stilar, samt förbättrar prestanda genom cache.


Fråga 2: CSS-selektorer

Fråga:
Förklara skillnaden mellan dessa selektorer och ge exempel på när du skulle använda var och en:

p { }
.highlight { }
#header { }
nav ul li { }

Svar:

  • p – Elementselektor: Väljer alla <p>-element. Används för grundläggande styling av paragrafer.
  • .highlight – Klassselektor: Väljer alla element med klassen highlight. Används för återanvändbara stilar.
  • #header – ID-selektor: Väljer elementet med id header. Används för unika element.
  • nav ul li – Ättlingselektor: Väljer alla <li>-element inuti en <ul> som ligger i en <nav>. Används för att styla specifika strukturer.

Fråga 3: Specificitet och kaskaden

Fråga:
Vilken färg får texten i detta exempel och varför?

<p id="viktigt" class="highlight">Text</p>
p { color: black; }
.highlight { color: blue; }
#viktigt { color: red; }
p.highlight { color: green; }

Svar:
Texten blir röd eftersom ID-selektorn #viktigt har högst specificitet och därför vinner över de andra reglerna.


Fråga 4: Box model

Fråga:
Förklara CSS box model. Om ett element har width: 200px, padding: 20px, border: 5px solid black och margin: 10px, vad blir elementets totala bredd?

Svar:
Box model består av content, padding, border och margin.
Total bredd = width + (padding × 2) + (border × 2) = 200 + 40 + 10 = 250px (margin räknas utanför detta).


Fråga 5: Display-egenskaper

Fråga:
Vad är skillnaden mellan display: block, display: inline och display: inline-block?

Svar:

  • block: Elementet tar hela raden, respekterar width/height, börjar på ny rad.
  • inline: Elementet tar bara så mycket plats som behövs, ignorerar width/height, bryter inte rad.
  • inline-block: Som inline, men respekterar width/height.

Fråga 6: CSS-positionering

Fråga:
Förklara skillnaden mellan position: relative, position: absolute och position: fixed. Ge exempel på användningsfall.

Svar:

  • relative: Elementet flyttas relativt sin ursprungliga plats, påverkar inte andra element.
  • absolute: Positioneras relativt närmaste positionerade förälder, tas ur flödet.
  • fixed: Positioneras relativt till fönstret, stannar kvar vid scroll.

Fråga 7: Färger och typografi

Fråga:
Förklara skillnaden mellan dessa färgformat och när du skulle använda dem:

color: red;
color: #FF0000;
color: rgb(255, 0, 0);
color: rgba(255, 0, 0, 0.5);

Svar:

  • red: Namngiven färg, enkel men begränsad.
  • #FF0000: HEX, vanligt för webben.
  • rgb(255, 0, 0): RGB, bra för exakta färger.
  • rgba(255, 0, 0, 0.5): RGB med transparens (alpha).

Fråga 8: Responsiv design – Media Queries

Fråga:
Vad är skillnaden mellan dessa två Media Queries och vilken strategi representerar de?

@media (max-width: 768px) { ... }
@media (min-width: 768px) { ... }

Svar:

  • max-width: Desktop-first, ändrar stilar för mindre skärmar.
  • min-width: Mobile-first, lägger till/förbättrar stilar för större skärmar.
    Mobile-first rekommenderas idag.

Fråga 9: Mobile-first design

Fråga:
Varför rekommenderas mobile-first design och hur implementerar du det i CSS?

Svar:
Mobile-first prioriterar prestanda och tillgänglighet för mobila användare.
Implementeras genom att skriva grundstilar utan media queries och lägga till förbättringar med @media (min-width: ...).


Fråga 10: Flexibla enheter

Fråga:
Förklara skillnaden mellan px, em, rem och %. När skulle du använda respektive enhet?

Svar:

  • px: Absolut, exakt storlek.
  • em: Relativ till förälderns font-size.
  • rem: Relativ till root-elementets font-size.
  • %: Relativ till förälderns storlek.
    Använd rem för typografi, % för layouter, px för detaljer.

Fråga 11: Flexbox layout

Fråga:
Förklara hur Flexbox fungerar. Vad är skillnaden mellan justify-content och align-items? Ge ett exempel.

Svar:
Flexbox är för endimensionella layouter.

  • justify-content: Justerar längs huvudaxeln (t.ex. horisontellt).
  • align-items: Justerar längs tväraxeln (t.ex. vertikalt).

Exempel:

.container {
  display: flex;
  justify-content: center;
  align-items: center;
}

Fråga 12: CSS Grid layout

Fråga:
Vad är skillnaden mellan CSS Grid och Flexbox? Visa hur du skapar en enkel 3-kolumns layout med Grid.

Svar:
Grid är tvådimensionellt (rader och kolumner), Flexbox är endimensionellt.
Exempel:

.container {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
}

Tips för tekniska intervjuer

  • Rita gärna box model eller layout-flöde.
  • Förklara varför du väljer en viss lösning.
  • Ge exempel från egna projekt.
  • Visa att du kan felsöka och resonera kring CSS-problem.

Kapitel 5: Fortsättning JavaScript - Asynkron Kod och Datahantering

I föregående kapitel lade vi grunden för JavaScript genom att manipulera DOM, hantera händelser och skapa enkel interaktivitet. Nu är det dags att dyka djupare in i mer avancerade, men helt nödvändiga, koncept för modern webbutveckling.

Varför detta kapitel är viktigt:

Webbapplikationer behöver ofta kommunicera med servrar för att hämta eller skicka data utan att hela sidan laddas om. De behöver också hantera operationer som kan ta tid (t.ex. vänta på svar från en server) utan att webbläsaren "fryser". Detta kräver förståelse för asynkron programmering.

Vi kommer också att arbeta mycket med datalistor och behöver effektiva sätt att transformera och filtrera dessa. Dessutom behöver vi ett standardiserat sätt att utbyta data mellan klient (webbläsare) och server.

Vad vi kommer att gå igenom:

  • Asynkron JavaScript: Förstå hur JavaScript hanterar tidskrävande operationer med händelseloopen (event loop) och återanrop (callbacks).
  • Promises: Ett modernare och mer robust sätt att hantera asynkrona operationer och undvika "callback hell".
  • Fetch API: Standardmetoden i moderna webbläsare för att göra nätverksanrop (hämta data från API:er).
  • JSON (JavaScript Object Notation): Det vanligaste dataformatet för datautbyte på webben.
  • Avancerade Array-metoder: Kraftfulla inbyggda metoder som map, filter, och reduce för att arbeta med arrayer på ett funktionellt sätt.

Detta kapitel bygger vidare på dina kunskaper och ger dig verktygen för att skapa mer dynamiska och datadrivna webbapplikationer.

Asynkron Programmering i JavaScript

I grund och botten är JavaScript ett single-threaded (entrådat) språk. Det betyder att det bara kan göra en sak i taget, steg för steg, i den ordning koden är skriven. Tänk dig en kock som bara kan hacka en grönsak åt gången innan hen går vidare till nästa.

Varför är detta viktigt?

Om JavaScript bara kan göra en sak i taget, vad händer när en uppgift tar lång tid? Till exempel:

  • Vänta på svar från en server (nätverksanrop).
  • Läsa en stor fil från hårddisken.
  • Komplexa beräkningar.
  • Vänta en viss tid (t.ex. med setTimeout).

I ett strikt synkront flöde skulle hela programmet (och ofta hela webbläsarens flik!) stanna upp och vänta tills den tidskrävande uppgiften är klar. Detta kallas blockering (blocking) och leder till en dålig användarupplevelse – sidan fryster och blir oresponsiv.

// Synkront exempel (simulerat)
console.log("Startar uppgift...");

// Simulerar en tidskrävande operation (blockerar i 5 sekunder)
const start = Date.now();
while (Date.now() < start + 5000) {
  // Gör ingenting, bara väntar...
}

console.log("Uppgiften klar!"); // Denna loggas först efter 5 sekunder
console.log("Annan kod körs..."); // Denna måste också vänta

Under de 5 sekunderna i exemplet ovan skulle webbsidan vara helt fryst.

Den Asynkrona Lösningen: Callback-funktioner

För att undvika blockering använder JavaScript (och dess körmiljöer som webbläsare och Node.js) en asynkron modell för tidskrävande operationer. Istället för att vänta, säger JavaScript: "Starta den här uppgiften, och när den är klar, kör den här specifika funktionen".

Denna specifika funktion som skickas med för att köras senare kallas en callback-funktion (återanropsfunktion).

console.log("Startar timer...");

// setTimeout är en asynkron funktion
setTimeout(() => {
  // Detta är callback-funktionen
  console.log("Timer klar efter 2 sekunder!");
}, 2000); // 2000 millisekunder = 2 sekunder

console.log("Timer startad, fortsätter med annan kod...");

// Output:
// Startar timer...
// Timer startad, fortsätter med annan kod...
// (efter 2 sekunder)
// Timer klar efter 2 sekunder!

Notera hur setTimeout inte blockerar. Koden fortsätter direkt till nästa console.log, och callback-funktionen körs först när timern har gått ut.

Händelseloopen (The Event Loop)

Men hur fungerar detta bakom kulisserna? Hur kan JavaScript, som är entrådat, hantera saker som händer "samtidigt"?

Nyckeln är händelseloopen (event loop) och samarbete mellan JavaScript-motorn och dess körmiljö (t.ex. webbläsaren).

  1. Call Stack (Anropsstacken): Här exekveras den vanliga, synkrona JavaScript-koden. Funktioner läggs på stacken när de anropas och tas bort när de är klara.
  2. Web APIs (Webbläsar-API:er): När JavaScript stöter på en asynkron operation (som setTimeout, fetch, DOM-händelser), skickas den vidare till ett webbläsar-API som hanterar den utanför JavaScripts huvudtråd.
  3. Callback Queue (Callback-kön) / Task Queue: När webbläsar-API:et är klart med sin uppgift (t.ex. timern har gått ut, data har hämtats), läggs den tillhörande callback-funktionen i en kö.
  4. Event Loop (Händelseloopen): Detta är en process som ständigt övervakar två saker: Är anropsstacken (Call Stack) tom? Finns det något i callback-kön?
  5. Exekvering: Om anropsstacken är tom och det finns en callback i kön, plockar händelseloopen den första callback-funktionen från kön och lägger den på anropsstacken för att köras.
graph TD
    subgraph JavaScript Engine
        CS[Call Stack]
    end

    subgraph Browser/Node APIs
        WA[Web APIs (setTimeout, fetch, DOM etc.)]
    end

    subgraph Task Queues
        CQ[Callback Queue (Tasks)]
        MQ[Microtask Queue (Promises etc.)] -- Högre prioritet --> EL
    end

    EL{Event Loop} -- Checks if Call Stack is empty --> CS

    CS -- Runs sync code --> CS
    CS -- Calls async func --> WA
    WA -- Callback ready --> CQ
    WA -- Promise resolved/rejected --> MQ

    EL -- Checks queues --> CQ
    EL -- Checks queues --> MQ

    MQ -- Has task & CS empty --> EL
    CQ -- Has task & CS empty & MQ empty --> EL

    EL -- Moves callback --> CS

    style WA fill:#f9d,stroke:#333,stroke-width:2px
    style CS fill:#ccf,stroke:#333,stroke-width:2px
    style CQ fill:#dfd,stroke:#333,stroke-width:2px
    style MQ fill:#ffe,stroke:#333,stroke-width:2px
    style EL fill:#eee,stroke:#333,stroke-width:4px

Diagrammet ovan illustrerar flödet: Synkron kod körs på Call Stack. Asynkrona operationer skickas till Web APIs. När de är klara hamnar deras callbacks i Callback Queue (för t.ex. setTimeout) eller Microtask Queue (för t.ex. Promises - dessa har högre prioritet). Event Loop kontrollerar kontinuerligt om Call Stack är tom och flyttar sedan uppgifter från köerna (Microtask först) till Call Stack för exekvering.

Analogi: Tänk dig en restaurangkock (JavaScript Call Stack) som bara kan laga en rätt i taget. När en gäst beställer något som tar lång tid (t.ex. en långkokt gryta - en asynkron operation), tar en servitör (Web API) beställningen och sätter igång grytan på en annan spis. Kocken fortsätter med andra snabba beställningar. När grytan är klar, säger servitören till via en bong (Callback läggs i kön). När kocken är klar med det hen höll på med (Call Stack är tom), tittar hen på bongarna (Event Loop kollar kön) och tar nästa färdiga rätt (Callback flyttas till Call Stack) för att lägga upp den.

Detta system gör att JavaScript kan hantera många operationer utan att blockera huvudtråden, vilket håller webbsidan responsiv.

Callback Hell

Callbacks är grundläggande, men när man har flera asynkrona operationer som beror på varandra kan det leda till djupt nästlad kod, ofta kallat "Callback Hell" eller "Pyramid of Doom".

// Exempel på Callback Hell (konceptuellt)
operation1((resultat1) => {
  console.log("Klar med 1");
  operation2(resultat1, (resultat2) => {
    console.log("Klar med 2");
    operation3(resultat2, (resultat3) => {
      console.log("Klar med 3");
      // ...och så vidare...
    });
  });
});

Denna kod blir svårläst och svår att underhålla. Detta är ett av huvudproblemen som Promises, vilka vi tittar på i nästa avsnitt, löser.

Promises och Async/Await

Som vi såg i föregående avsnitt kan callbacks leda till rörig kod, speciellt när flera asynkrona operationer måste utföras i sekvens ("Callback Hell"). För att lösa detta introducerades Promises (Löften) i JavaScript.

Vad är ett Promise?

Ett Promise representerar det framtida resultatet av en asynkron operation. Det är ett objekt som fungerar som en platshållare för ett värde som ännu inte är känt, men som lovar att leverera ett värde (eller ett fel) vid någon tidpunkt i framtiden.

Tänk på det som att beställa en bok online. Du får en orderbekräftelse (ett Promise) direkt. Boken (värdet) har inte kommit än, men orderbekräftelsen lovar att du antingen kommer att få boken (Promise fulfilled) eller ett meddelande om att något gick fel, t.ex. att boken var slutsåld (Promise rejected).

Promise-livscykel

Ett Promise kan befinna sig i ett av tre tillstånd:

  1. Pending (Väntande): Det initiala tillståndet. Operationen har startat men är ännu inte slutförd eller misslyckad.
  2. Fulfilled (Uppfylld): Operationen slutfördes framgångsrikt. Promiset har nu ett resulterande värde.
  3. Rejected (Avvisad): Operationen misslyckades. Promiset har nu en anledning (ett felobjekt) till varför det misslyckades.

Ett Promise kan bara övergå från pending till antingen fulfilled eller rejected, och när det väl har hänt ändras dess tillstånd aldrig igen. Man säger att Promiset är settled (avgjort).

Att Använda Promises: .then() och .catch()

För att hantera resultatet (eller felet) av ett Promise använder vi metoderna .then() och .catch().

  • .then(onFulfilled, onRejected): Tar emot upp till två argument, båda funktioner:
    • onFulfilled: Körs om Promiset blir fulfilled. Den tar emot det resulterande värdet som argument.
    • onRejected: Körs om Promiset blir rejected. Den tar emot felorsaken som argument. (Det är vanligare att använda .catch() för detta).
  • .catch(onRejected): Tar emot en funktion som körs om Promiset blir rejected. Det är ett mer läsbart sätt att hantera fel än att skicka en andra funktion till .then().
// Simulerar en asynkron operation som returnerar ett Promise
function fetchData() {
  return new Promise((resolve, reject) => {
    // Simulera nätverksfördröjning
    setTimeout(() => {
      const success = Math.random() > 0.3; // 70% chans att lyckas
      if (success) {
        resolve({ data: "Hämtad data!" }); // Uppfyll Promiset med data
      } else {
        reject(new Error("Kunde inte hämta data.")); // Avvisa Promiset med ett fel
      }
    }, 1500);
  });
}

console.log("Startar datahämtning...");

fetchData()
  .then((result) => {
    // Körs om Promiset blir fulfilled
    console.log("Success:", result.data);
  })
  .catch((error) => {
    // Körs om Promiset blir rejected
    console.error("Error:", error.message);
  })
  .finally(() => {
    // Körs alltid, oavsett om det lyckades eller misslyckades
    console.log("Datahämtning avslutad (fulfilled eller rejected).");
  });

console.log("Promise startat, väntar på resultat...");

// Output (exempel vid success):
// Startar datahämtning...
// Promise startat, väntar på resultat...
// (efter 1.5 sekunder)
// Success: Hämtad data!
// Datahämtning avslutad (fulfilled eller rejected).

// Output (exempel vid error):
// Startar datahämtning...
// Promise startat, väntar på resultat...
// (efter 1.5 sekunder)
// Error: Kunde inte hämta data.
// Datahämtning avslutad (fulfilled eller rejected).

Notera .finally() som körs oavsett utfall, användbart för t.ex. städning eller att dölja en laddningsindikator.

Kedjade Promises (Chaining)

Styrkan med .then() är att den också returnerar ett Promise. Detta gör att vi kan kedja flera asynkrona operationer efter varandra på ett läsbart sätt, utan djup nästling.

fetchData() // Returnerar ett Promise
  .then(result1 => {
    console.log("Steg 1 klart:", result1.data);
    // Starta nästa operation, som också returnerar ett Promise
    return anotherAsyncOperation(result1.data);
  })
  .then(result2 => {
    console.log("Steg 2 klart:", result2);
    // ...och så vidare...
  })
  .catch(error => {
    // Ett enda catch hanterar fel från *alla* föregående steg i kedjan
    console.error("Ett fel inträffade i kedjan:", error.message);
  });

Async/Await: Syntaktiskt Socker

Även om kedjade Promises är mycket bättre än Callback Hell, kan koden fortfarande bli lite svårläst med många .then(). ES2017 introducerade async och await för att skriva asynkron kod som ser ut som synkron kod.

  • async function: När du deklarerar en funktion med async framför, händer två saker:
    1. Funktionen returnerar alltid automatiskt ett Promise. Om funktionen returnerar ett värde, blir det värdet resultatet av ett fulfilled Promise. Om funktionen kastar ett fel, blir det felet anledningen till ett rejected Promise.
    2. Det möjliggör användningen av await inuti funktionen.
  • await: Kan endast användas inuti en async function. När du sätter await framför ett anrop som returnerar ett Promise, pausas exekveringen av async-funktionen (utan att blockera huvudtråden!) tills det Promiset är settled (avgjort).
    • Om Promiset blir fulfilled, returnerar await-uttrycket det uppfyllda värdet.
    • Om Promiset blir rejected, kastar await-uttrycket felet (som kan fångas med try...catch).
// Samma fetchData som tidigare

// async funktion för att använda await
async function processData() {
  console.log("Startar processData (async)...");
  try {
    // Pausar här tills fetchData() är fulfilled eller rejected
    const result = await fetchData();
    console.log("Data hämtad (inom async):");

    // Kan nu använda resultatet som om det vore synkront
    const processedData = result.data.toUpperCase();
    console.log("Bearbetad data:", processedData);

    // Om vi hade fler await-anrop skulle de köras sekventiellt
    // const result2 = await anotherAsyncOperation(processedData);
    // console.log("Resultat från andra operationen:", result2);

    return processedData; // Detta blir värdet för Promiset som processData returnerar

  } catch (error) {
    // Fångar fel från await fetchData() eller andra fel i try-blocket
    console.error("Fel i processData:", error.message);
    // Kan välja att kasta felet vidare eller returnera ett standardvärde
    throw error; // Kasta om felet så att anroparen kan fånga det
  }
}

console.log("Anropar processData...");

// Eftersom processData är async, returnerar den ett Promise
processData()
  .then(finalResult => {
    console.log("processData lyckades med resultat:", finalResult);
  })
  .catch(error => {
    console.error("processData misslyckades:", error.message);
  });

console.log("processData anropad, väntar...");

Fördelar med Async/Await:

  • Läslighet: Koden ser mycket mer ut som traditionell, synkron kod.
  • Felhantering: Använder standard try...catch, vilket många utvecklare är vana vid.
  • Enklare felsökning: Lättare att följa kodflödet och sätta brytpunkter.

async/await är idag det vanligaste sättet att hantera Promises i modern JavaScript-utveckling.

Hämta Data med Fetch API

Nu när vi förstår Promises och async/await, kan vi titta på det moderna sättet att göra nätverksanrop (network requests) i JavaScript: Fetch API.

Fetch API är ett inbyggt gränssnitt i webbläsaren (och Node.js via externa bibliotek) som ger ett kraftfullt och flexibelt sätt att interagera med resurser över nätverket, oftast genom att hämta data från eller skicka data till ett webb-API (Application Programming Interface).

Varför Fetch?

  • Modernt och Standard: Det är det rekommenderade sättet att göra AJAX (Asynchronous JavaScript and XML) -liknande anrop idag, och ersätter den äldre XMLHttpRequest.
  • Promise-baserat: Det använder Promises naturligt, vilket gör det lätt att integrera med async/await och .then()/.catch().
  • Flexibelt: Ger finmaskig kontroll över request (förfrågan) och response (svar).

Grundläggande Användning: GET-Request

Den vanligaste användningen är att hämta data med en HTTP GET-metod. Detta görs enkelt genom att skicka URL:en till resursen som argument till fetch().

// URL till ett publikt API (JSONPlaceholder - simulerar en blogg)
const apiUrl = 'https://jsonplaceholder.typicode.com/posts/1';

console.log("Startar fetch-anrop...");

fetch(apiUrl)
  .then(response => {
    // Steg 1: Hantera HTTP-svaret
    console.log("Mottagit response:", response.status, response.statusText);

    // fetch() kastar inte automatiskt fel vid HTTP-fel (som 404 eller 500)
    // Vi måste kontrollera response.ok (true om status är 200-299)
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    // Om svaret är OK, behöver vi konvertera body till det format vi vill ha,
    // oftast JSON. response.json() returnerar *också* ett Promise!
    return response.json();
  })
  .then(data => {
    // Steg 2: Hantera den parsade datan (nu ett JavaScript-objekt)
    console.log("Mottagen data:", data);
    // Gör något med datan, t.ex. visa den på sidan
    document.body.innerHTML += `<p>Titel: ${data.title}</p>`;
  })
  .catch(error => {
    // Hantera fel från nätverket eller från throw new Error ovan
    console.error("Fel vid fetch:", error);
    document.body.innerHTML += `<p style="color: red;">Kunde inte hämta data: ${error.message}</p>`;
  });

console.log("Fetch-anrop startat, väntar...");

Viktiga Steg:

  1. Anropa fetch(url). Detta returnerar ett Promise som blir fulfilled så snart webbläsaren får tillbaka headers från servern.
  2. I första .then(), kontrollera response.ok. Om inte OK, kasta ett fel (throw new Error(...)) så att det fångas av .catch().
  3. Om OK, använd en metod på response-objektet för att läsa body (själva datan). De vanligaste är:
    • response.json(): Tolkar body som JSON och returnerar ett Promise som blir fulfilled med det parsade JavaScript-objektet.
    • response.text(): Returnerar ett Promise som blir fulfilled med body som en textsträng.
    • response.blob(): För binärdata (t.ex. bilder).
    • response.formData(): För formulärdata.
  4. I nästa .then() hanterar du den faktiska datan (t.ex. det parsade JSON-objektet).
  5. Använd .catch() för att hantera eventuella nätverksfel (t.ex. ingen anslutning) eller de fel du själv kastade (t.ex. vid response.ok === false).

Använda med Async/Await

Fetch blir ännu smidigare med async/await:

async function getPost() {
  const apiUrl = 'https://jsonplaceholder.typicode.com/posts/1';
  console.log("Startar async fetch...");
  try {
    const response = await fetch(apiUrl);
    console.log("Mottagit response (async):", response.status, response.statusText);

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    // Vänta på att body ska läsas och parsas som JSON
    const data = await response.json();
    console.log("Mottagen data (async):", data);

    document.body.innerHTML += `<p>Titel (async): ${data.title}</p>`;

  } catch (error) {
    console.error("Fel vid async fetch:", error);
    document.body.innerHTML += `<p style="color: red;">Kunde inte hämta data (async): ${error.message}</p>`;
  }
}

getPost();
console.log("Async fetch startad, väntar...");

Mer Avancerad Fetch: Options

fetch() kan ta ett andra argument, ett options-objekt, för att anpassa förfrågan. Detta är nödvändigt för andra HTTP-metoder (som POST, PUT, DELETE) eller för att skicka med data och headers.

async function createPost(newPostData) {
  const apiUrl = 'https://jsonplaceholder.typicode.com/posts';
  try {
    const response = await fetch(apiUrl, {
      method: 'POST', // Ange HTTP-metod
      headers: {
        'Content-Type': 'application/json' // Tala om att vi skickar JSON
        // Lägg till andra headers här om det behövs (t.ex. Authorization)
      },
      body: JSON.stringify(newPostData) // Konvertera JS-objekt till JSON-sträng
    });

    if (!response.ok) {
      // För POST kan man vilja läsa felmeddelande från servern
      const errorData = await response.json().catch(() => ({})); // Försök läsa fel, annars tomt objekt
      throw new Error(`HTTP error! status: ${response.status} - ${errorData.message || response.statusText}`);
    }

    // För POST (status 201 Created), får vi oftast tillbaka det skapade objektet
    const createdPost = await response.json();
    console.log('Skapad post:', createdPost);
    return createdPost;

  } catch (error) {
    console.error('Fel vid POST-request:', error);
    // Hantera felet, visa meddelande för användaren etc.
    throw error; // Kasta vidare om anroparen behöver veta
  }
}

// Exempel på användning
const myNewPost = {
  title: 'Min Nya Bloggpost',
  body: 'Detta är innehållet.',
  userId: 1
};

createPost(myNewPost)
  .then(post => console.log(`Post med id ${post.id} skapades.`))
  .catch(err => console.log("Kunde inte skapa post."));

Vanliga Options:

  • method: HTTP-metod ('GET', 'POST', 'PUT', 'DELETE', 'PATCH', etc.). Standard är 'GET'.
  • headers: Ett objekt med HTTP-headers (nyckel-värde-par) att skicka med förfrågan. 'Content-Type': 'application/json' är mycket vanlig när man skickar JSON-data.
  • body: Datan som ska skickas med förfrågan (t.ex. för POST eller PUT). Måste oftast vara en sträng (använd JSON.stringify() för objekt) eller FormData, Blob, etc.
  • mode: ('cors', 'no-cors', 'same-origin'). Styr hur Cross-Origin Resource Sharing (CORS) hanteras. 'cors' är standard och vanligast för API-anrop.
  • credentials: ('include', 'same-origin', 'omit'). Styr om cookies ska skickas med förfrågan.

Fetch API är ett kraftfullt verktyg för att bygga dynamiska webbapplikationer som interagerar med externa datakällor.

Dataformat: JSON

När vi hämtar data från ett API med fetch, eller när vi vill skicka data till ett API, behöver vi ett standardiserat format för att representera den datan som text. Det absolut vanligaste formatet för detta på webben idag är JSON (JavaScript Object Notation).

Varför JSON?

  • Läsbart: Det är relativt lätt för människor att läsa och skriva.
  • Lättviktigt: Det är ett kompakt textformat, vilket är effektivt för dataöverföring.
  • JavaScript-vänligt: Som namnet antyder är syntaxen väldigt lik JavaScripts objektsyntax, vilket gör det extremt enkelt att arbeta med i JavaScript.

Alternativet: XML

Tidigare var XML (eXtensible Markup Language) ett vanligt format för datautbyte. XML använder taggar, liknande HTML, för att strukturera data.

<!-- Exempel på data i XML-format -->
<post>
  <userId>1</userId>
  <id>1</id>
  <title>Detta är titeln</title>
  <body>Detta är innehållet.</body>
</post>

Även om du kan stöta på XML i vissa äldre system eller specifika branscher, är JSON det dominerande formatet för webb-API:er idag. Att parsa (tolka) XML i JavaScript kräver oftast separata bibliotek eller mer komplex kod än att hantera JSON.

JSON Syntax

JSON-data representeras som text och följer några enkla regler:

  • Objekt: Representeras med krullparenteser {}. Innehåller nyckel-värde-par.
    • Nycklar måste vara strängar inom dubbla citationstecken ("key").
    • Värden kan vara strängar, nummer, booleans (true/false), null, arrayer, eller andra JSON-objekt.
    • Nyckel och värde separeras av ett kolon :. Paren separeras av kommatecken ,.
  • Arrayer: Representeras med hakparenteser []. Innehåller en ordnad lista av värden, separerade av kommatecken.
  • Strängar: Måste använda dubbla citationstecken ("text").
  • Nummer: Vanliga tal (heltal eller decimaltal).
  • Booleans: true eller false (små bokstäver).
  • Null: null (små bokstäver).

Viktigt: JSON tillåter inte funktioner, undefined, kommentarer, eller avslutande kommatecken (trailing commas).

// Exempel på JSON-data (som text)
{
  "userId": 1,
  "id": 1,
  "title": "Exempeltitel",
  "body": "Detta är innehållet i posten.",
  "tags": ["webbutveckling", "javascript", "json"],
  "metadata": {
    "published": true,
    "views": 1050,
    "author": null
  }
}

Arbeta med JSON i JavaScript

JavaScript har ett inbyggt globalt JSON-objekt med två huvudsakliga metoder för att konvertera mellan JSON-text och JavaScript-objekt/-värden.

JSON.stringify() - Från JavaScript till JSON-sträng

Denna metod tar ett JavaScript-värde (oftast ett objekt eller en array) och omvandlar det till en JSON-formaterad sträng. Detta används när du ska skicka data till ett API (t.ex. i body för en fetch POST-request).

const postData = {
  title: "Min Nya Bloggpost",
  body: "Detta är innehållet.",
  userId: 1,
  published: false, // JavaScript boolean
  tags: ['ny', 'test'] // JavaScript array
};

// Konvertera till JSON-sträng
const jsonString = JSON.stringify(postData);

console.log(jsonString);
// Output: "{"title":"Min Nya Bloggpost","body":"Detta är innehållet.","userId":1,"published":false,"tags":["ny","test"]}"

// Snyggare formatering (indentering)
const prettyJsonString = JSON.stringify(postData, null, 2); // Använd 2 mellanslag för indentering
console.log("\nSnygg JSON:");
console.log(prettyJsonString);
/* Output:
{
  "title": "Min Nya Bloggpost",
  "body": "Detta är innehållet.",
  "userId": 1,
  "published": false,
  "tags": [
    "ny",
    "test"
  ]
}
*/

// Vad händer med otillåtna värden?
const complexObject = {
  name: "Test",
  id: 123,
  action: function() { console.log('Hej!'); }, // Funktioner ignoreras
  value: undefined // undefined blir ignorerat (eller null i arrayer)
};
console.log("\nKomplext objekt:", JSON.stringify(complexObject));
// Output: "{"name":"Test","id":123}"

JSON.parse() - Från JSON-sträng till JavaScript

Denna metod tar en JSON-formaterad sträng och omvandlar den tillbaka till ett motsvarande JavaScript-värde (objekt, array, sträng, nummer, boolean, null). Detta används när du har tagit emot data från ett API (t.ex. resultatet från response.json() i fetch).

const receivedJsonString = '{\n  "postId": 101,\n  "title": "Data från servern",\n  "tags": ["api", "data"],\n  "isPublic": true\n}';

try {
  // Försök att parsa JSON-strängen
  const jsObject = JSON.parse(receivedJsonString);

  console.log("Parsat objekt:", jsObject);
  console.log("Titel:", jsObject.title); // "Data från servern"
  console.log("Första taggen:", jsObject.tags[0]); // "api"
  console.log("Är publik:", jsObject.isPublic); // true

} catch (error) {
  // Felhantering om JSON-strängen är ogiltig
  console.error("Kunde inte parsa JSON:", error);
}

// Exempel med ogiltig JSON
const invalidJsonString = '{\n  "name": "Test",\n  "value": 123, // Ogiltigt: avslutande kommatecken
}';

try {
  const invalidObject = JSON.parse(invalidJsonString);
  console.log("Ogiltigt objekt:", invalidObject);
} catch (error) {
  console.error("\nFel vid parsning av ogiltig JSON:", error.message);
  // Output: Fel vid parsning av ogiltig JSON: Unexpected token } in JSON at position ...
}

Viktigt med Felhantering: Eftersom JSON-data ofta kommer från externa källor, kan den vara felaktigt formaterad. Det är därför viktigt att alltid omsluta JSON.parse() i ett try...catch-block för att hantera eventuella SyntaxError som kan kastas om parsningen misslyckas.

Data Transformation och Validering

När du har parsat JSON-data till ett JavaScript-objekt, är det ofta bara det första steget. Därefter kan du behöva:

  • Transformera datan: Ändra strukturen, filtrera bort onödig information, kombinera med annan data, etc. Här kommer ofta de avancerade array-metoderna (som vi ska titta på härnäst) till stor nytta.
  • Validera datan: Kontrollera att datan du fick faktiskt innehåller de fält och datatyper du förväntar dig innan du använder den i din applikation. Detta skyddar mot oväntade fel om API:et ändras eller skickar oväntad data.

Att förstå JSON är fundamentalt för att kunna bygga webbapplikationer som kommunicerar med omvärlden.

Avancerade Array-Metoder och Objekt-Syntax

När vi arbetar med data, särskilt listor (arrayer) som vi ofta får tillbaka från API:er, behöver vi effektiva sätt att transformera, filtrera och sammanställa den datan. JavaScript erbjuder flera kraftfulla inbyggda array-metoder som hjälper oss med detta på ett deklarativt och läsbart sätt.

Vi kommer också att titta på modern syntax för att arbeta med objekt och arrayer, vilket gör koden mer koncis.

map() - Transformera Element

map är en inbyggd array-metod som skapar en ny array genom att utföra en angiven funktion på varje element i den ursprungliga arrayen. Den ursprungliga arrayen förblir oförändrad, medan den nya arrayen innehåller de transformerade värdena. map är särskilt användbar när du vill bearbeta data och skapa en ny version av den utan att ändra originalet.

map tar en callback-funktion som parameter, och denna funktion körs en gång för varje element. Callback-funktionen kan ta upp till tre argument:

  1. element: Värdet på det aktuella elementet som bearbetas.
  2. index (valfri): Indexet för det aktuella elementet.
  3. array (valfri): Den ursprungliga arrayen som map anropades på.

Det vanligaste är dock att bara använda det första argumentet (element). Callback-funktionen måste returnera det nya värdet för elementet i den nya arrayen.

const numbers = [1, 2, 3, 4, 5];

// Skapa en ny array där varje nummer är dubblerat
const doubled = numbers.map(number => number * 2);
console.log(doubled); // Output: [2, 4, 6, 8, 10]
console.log(numbers); // Output: [1, 2, 3, 4, 5] (originalet är oförändrat)

const names = ["anna", "bertil", "cesar"];

// Skapa en ny array med namnen omvandlade till stor begynnelsebokstav
const capitalized = names.map(name => name.charAt(0).toUpperCase() + name.slice(1));
console.log(capitalized); // Output: ["Anna", "Bertil", "Cesar"]

const products = [
  { id: "p1", name: "Laptop", price: 12000 },
  { id: "p2", name: "Phone", price: 7500 },
  { id: "p3", name: "Tablet", price: 4000 }
];

// Skapa en ny array som bara innehåller produktpriserna
const prices = products.map(product => product.price);
console.log(prices); // Output: [12000, 7500, 4000]

// Skapa en ny array med produktobjekt som har priset inkl. moms (25%)
const productsWithVat = products.map(product => {
  return {
    ...product, // Kopiera alla befintliga egenskaper (mer om '...' senare)
    priceWithVat: product.price * 1.25
  };
});
console.log(productsWithVat);
/* Output:
[
  { id: 'p1', name: 'Laptop', price: 12000, priceWithVat: 15000 },
  { id: 'p2', name: 'Phone', price: 7500, priceWithVat: 9375 },
  { id: 'p3', name: 'Tablet', price: 4000, priceWithVat: 5000 }
]
*/

filter() - Välja ut Element

filter är en annan array-metod som skapar en ny array. Denna nya array innehåller endast de element från den ursprungliga arrayen som uppfyller ett visst villkor. Den ursprungliga arrayen förblir oförändrad.

Precis som map, tar filter en callback-funktion som körs för varje element. Callback-funktionen tar samma argument (element, index, array). Skillnaden är att callback-funktionen här måste returnera ett boolean-värde (true eller false).

  • Om callback-funktionen returnerar true, inkluderas elementet i den nya arrayen.
  • Om callback-funktionen returnerar false, exkluderas elementet.
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

// Skapa en ny array med endast jämna nummer
const evenNumbers = numbers.filter(number => number % 2 === 0);
console.log(evenNumbers); // Output: [2, 4, 6, 8, 10]
console.log(numbers); // Output: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

const names = ["Anna", "Bertil", "Cesar", "David", "Eva"];

// Skapa en ny array med namn som är längre än 4 bokstäver
const longNames = names.filter(name => name.length > 4);
console.log(longNames); // Output: ["Bertil", "Cesar", "David"]

const products = [
  { id: "p1", name: "Laptop", price: 12000, inStock: true },
  { id: "p2", name: "Phone", price: 7500, inStock: false },
  { id: "p3", name: "Tablet", price: 4000, inStock: true },
  { id: "p4", name: "Mouse", price: 300, inStock: true }
];

// Skapa en ny array med produkter som finns i lager
const availableProducts = products.filter(product => product.inStock);
console.log(availableProducts);
/* Output:
[
  { id: 'p1', name: 'Laptop', price: 12000, inStock: true },
  { id: 'p3', name: 'Tablet', price: 4000, inStock: true },
  { id: 'p4', name: 'Mouse', price: 300, inStock: true }
]
*/

// Kombinera filter och map: Hämta namnen på dyra produkter (pris > 5000)
const expensiveProductNames = products
  .filter(product => product.price > 5000) // Först filtrera
  .map(product => product.name);           // Sedan mappa resultatet
console.log(expensiveProductNames); // Output: ["Laptop", "Phone"]

reduce() - Sammanställa till Ett Värde

reduce är kanske den mest mångsidiga, men också den mest komplexa, av de vanliga array-metoderna. Den används för att "reducera" en array till ett enda värde genom att iterativt applicera en funktion på elementen. Detta enda värde kan vara ett nummer (t.ex. en summa), en sträng, ett objekt, eller till och med en annan array.

reduce tar två argument:

  1. En reducer-funktion (callback). Denna funktion tar i sin tur (oftast) fyra argument:
    • accumulator (ackumulator): Värdet som byggs upp och returneras från föregående iteration (eller initialValue vid första iterationen).
    • currentValue (aktuellt värde): Värdet på det aktuella elementet som bearbetas.
    • currentIndex (valfri): Indexet för det aktuella elementet.
    • array (valfri): Den ursprungliga arrayen. Reducer-funktionen måste returnera det uppdaterade ackumulatorvärdet för nästa iteration.
  2. Ett initialValue (initialvärde, valfritt): Värdet som ackumulatorn ska starta med vid den första iterationen. Om detta utelämnas används det första elementet i arrayen som initialvärde, och iterationen börjar från det andra elementet.
const numbers = [1, 2, 3, 4, 5];

// Beräkna summan av alla nummer
const sum = numbers.reduce((accumulator, currentValue) => {
  console.log(`Ack: ${accumulator}, Current: ${currentValue}`);
  return accumulator + currentValue;
}, 0); // Startvärde för ackumulatorn är 0

console.log("Summa:", sum); // Output: 15
/* Logg från reduce:
Ack: 0, Current: 1
Ack: 1, Current: 2
Ack: 3, Current: 3
Ack: 6, Current: 4
Ack: 10, Current: 5
*/

// Hitta det högsta värdet
const max = numbers.reduce((maxSoFar, current) => {
  return current > maxSoFar ? current : maxSoFar;
}); // Inget initialValue, första elementet (1) används som start
console.log("Max:", max); // Output: 5

const products = [
  { id: "p1", name: "Laptop", price: 12000 },
  { id: "p2", name: "Phone", price: 7500 },
  { id: "p3", name: "Tablet", price: 4000 }
];

// Beräkna totalt lagervärde
const totalValue = products.reduce((total, product) => total + product.price, 0);
console.log("Totalt värde:", totalValue); // Output: 23500

// Gruppera produkter efter prisklass (exempel på att returnera ett objekt)
const groupedByPrice = products.reduce((groups, product) => {
  const key = product.price > 5000 ? 'expensive' : 'cheap';
  if (!groups[key]) {
    groups[key] = []; // Skapa arrayen om den inte finns
  }
  groups[key].push(product);
  return groups; // Returnera det uppdaterade objektet
}, {}); // Startvärde är ett tomt objekt

console.log("Grupperade produkter:", groupedByPrice);
/* Output:
{
  expensive: [
    { id: 'p1', name: 'Laptop', price: 12000 },
    { id: 'p2', name: 'Phone', price: 7500 }
  ],
  cheap: [ { id: 'p3', name: 'Tablet', price: 4000 } ]
}
*/

Modern Objekt- och Array-Syntax

ES6 (ECMAScript 2015) och senare versioner av JavaScript har introducerat syntax som gör det smidigare att arbeta med objekt och arrayer.

Destructuring (Destrukturering)

Destructuring låter oss "packa upp" värden från arrayer eller egenskaper från objekt till separata variabler på ett koncis sätt.

// Array Destructuring
const coordinates = [10, 25, 5];
const [x, y, z] = coordinates;
console.log(x, y, z); // Output: 10 25 5

// Object Destructuring
const user = {
  id: "u1",
  name: "Alice",
  email: "alice@example.com",
  settings: { theme: "dark", notifications: true }
};

// Plocka ut egenskaper till variabler med samma namn
const { name, email } = user;
console.log(name, email); // Output: Alice alice@example.com

// Plocka ut och byt namn på variabeln
const { id: userId } = user;
console.log(userId); // Output: u1

// Plocka ut nästlade egenskaper
const { settings: { theme } } = user;
console.log(theme); // Output: dark

// Användbart i funktionsparametrar
function printUserName({ name }) {
  console.log(`Användarnamn: ${name}`);
}
printUserName(user); // Output: Användarnamn: Alice

Spread Operator (...)

Spread-operatorn låter oss "sprida ut" elementen från en array eller egenskaperna från ett objekt in i en ny array eller ett nytt objekt.

// Spread i Arrayer
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const combinedArr = [...arr1, 0, ...arr2]; // Sprid ut elementen från arr1 och arr2
console.log(combinedArr); // Output: [1, 2, 3, 0, 4, 5, 6]

// Skapa en kopia av en array
const arr1Copy = [...arr1];
console.log(arr1Copy); // Output: [1, 2, 3]
arr1Copy.push(4);
console.log(arr1); // Output: [1, 2, 3] (originalet oförändrat)

// Spread i Objekt (ES2018+)
const obj1 = { a: 1, b: 2 };
const obj2 = { b: 3, c: 4 };
const combinedObj = { ...obj1, ...obj2 }; // Egenskaper från obj2 skriver över de från obj1 vid namnkonflikt (b)
console.log(combinedObj); // Output: { a: 1, b: 3, c: 4 }

// Skapa en kopia av ett objekt
const obj1Copy = { ...obj1 };
console.log(obj1Copy); // Output: { a: 1, b: 2 }

// Lägga till/uppdatera egenskaper på ett icke-muterande sätt (vanligt i React/Redux)
const updatedObj1 = { ...obj1, b: 100, d: 5 };
console.log(updatedObj1); // Output: { a: 1, b: 100, d: 5 }
console.log(obj1); // Output: { a: 1, b: 2 } (originalet oförändrat)

Rest Parameters (...)

Rest-parametern ser likadan ut (...) men används i funktionsdefinitioner för att samla ihop ett obestämt antal argument till en array.

// Samla alla argument till en array
function sumAll(...numbers) { // numbers blir en array
  console.log(numbers); // T.ex. [1, 2, 3] eller [10, 20]
  return numbers.reduce((sum, num) => sum + num, 0);
}

console.log(sumAll(1, 2, 3)); // Output: 6
console.log(sumAll(10, 20));   // Output: 30

// Kombinera vanliga parametrar med rest
function logMessage(level, ...messages) { // messages blir en array med resten
  console.log(`[${level.toUpperCase()}]`, ...messages); // Kan sprida ut messages igen vid loggning
}

logMessage('info', 'Användare loggade in', 'ID: 123'); // Output: [INFO] Användare loggade in ID: 123
logMessage('error', 'Databasfel');              // Output: [ERROR] Databasfel

Dessa metoder och syntax-förbättringar (map, filter, reduce, destructuring, spread/rest) är centrala i modern JavaScript och gör det möjligt att skriva mer uttrycksfull, koncis och ofta mer läsbar kod, särskilt när man hanterar datastrukturer.

Praktiska Övningar: Asynkron JavaScript och API-Anrop

Nu är det dags att praktisera de kraftfulla koncept vi har lärt oss i detta kapitel. Vi kommer att använda fetch för att hämta data från det publika test-API:et JSONPlaceholder och sedan bearbeta och visa den datan med hjälp av array-metoder och DOM-manipulation.

Förutsättningar:

  • En enkel HTML-fil (index.html) där du kan visa resultaten.
  • En länkad JavaScript-fil (script.js) där du skriver din kod.
  • Grundläggande kunskaper i DOM-manipulation (t.ex. querySelector, createElement, textContent, appendChild) från föregående kapitel.

Tips: Använd async/await för att hantera dina fetch-anrop, då det ofta ger mer läsbar kod. Glöm inte try...catch för felhantering!

Övning 1: Hämta och Visa Användare

Mål: Hämta en lista med användare från JSONPlaceholder och visa deras namn i en lista på webbsidan.

  1. Skapa HTML-struktur: Lägg till en <ul>-tagg i din index.html med ett id, t.ex. id="user-list".
  2. Hämta Användare (i script.js):
    • Skapa en async function, t.ex. fetchUsers().
    • Använd fetch för att göra ett GET-anrop till https://jsonplaceholder.typicode.com/users.
    • Hantera response-objektet: Kontrollera response.ok och använd await response.json() för att få datan.
    • Använd try...catch för att fånga eventuella fel under hämtningen.
  3. Visa Namnen:
    • Inuti fetchUsers() (efter att du har fått datan): Hämta referensen till din <ul>-tagg.
    • Loopa igenom arrayen med användardata (som du fick från API:et).
    • För varje användare, skapa ett nytt <li>-element.
    • Sätt <li>-elementets textContent till användarens name.
    • Lägg till (appendChild) <li>-elementet i din <ul>.
  4. Anropa Funktionen: Glöm inte att anropa din fetchUsers()-funktion så att koden körs när sidan laddas.
  5. Testa: Öppna index.html i webbläsaren. Ser du en lista med 10 användarnamn?

Övning 2: Filtrera och Mappa Todos

Mål: Hämta en lista med "todos" (att-göra-uppgifter) från JSONPlaceholder. Filtrera listan så att du bara har de slutförda (completed: true) uppgifterna. Visa titlarna på dessa slutförda uppgifter i en ny lista.

  1. Skapa HTML-struktur: Lägg till en ny <ul>-tagg, t.ex. id="completed-todos".
  2. Hämta Todos (i script.js):
    • Skapa en ny async function, t.ex. fetchAndFilterTodos().
    • Använd fetch för att hämta data från https://jsonplaceholder.typicode.com/todos.
    • Hantera response och parsa JSON som i övning 1, inkludera try...catch.
  3. Filtrera och Mappa:
    • När du har fått todo-arrayen:
      • Använd filter()-metoden för att skapa en ny array som endast innehåller de objekt där egenskapen completed är true.
      • Använd map()-metoden på den filtrerade arrayen för att skapa en ny array som endast innehåller title för varje slutförd todo.
  4. Visa Titlarna:
    • Hämta referensen till din <ul>-tagg (#completed-todos).
    • Loopa igenom arrayen med titlar.
    • Skapa och lägg till <li>-element för varje titel i <ul>.
  5. Anropa Funktionen: Anropa fetchAndFilterTodos().
  6. Testa: Öppna index.html. Ser du en lista med titlarna på de slutförda uppgifterna?

Övning 3: Kombinera Data (Post och Kommentarer)

Mål: Hämta först en specifik bloggpost (t.ex. post med id 5). När du har hämtat posten, använd dess id för att hämta alla kommentarer som hör till just den posten. Visa postens titel och sedan en lista med kommentarernas namn (namnet på den som kommenterat).

  1. Skapa HTML-struktur: Lägg till en <div>, t.ex. id="post-and-comments".
  2. Hämta Data (i script.js):
    • Skapa en async function, t.ex. fetchPostAndComments(postId).
    • Steg 1: Hämta Posten:
      • Använd fetch för att hämta data från https://jsonplaceholder.typicode.com/posts/${postId} (använd template literal för att inkludera postId).
      • Hantera response och parsa JSON. Spara post-objektet.
    • Steg 2: Hämta Kommentarer:
      • Använd fetch igen, nu för att hämta data från https://jsonplaceholder.typicode.com/posts/${postId}/comments (eller https://jsonplaceholder.typicode.com/comments?postId=${postId}).
      • Hantera response och parsa JSON. Spara kommentars-arrayen.
    • Använd try...catch för att hantera fel från båda anropen.
  3. Visa Resultatet:
    • Hämta referensen till din <div> (#post-and-comments).
    • Skapa och lägg till ett <h2>-element med postens title.
    • Skapa en <ul>-lista för kommentarerna.
    • Loopa igenom kommentars-arrayen.
    • För varje kommentar, skapa ett <li>-element med kommentarens name och lägg till det i <ul>.
    • Lägg till <ul>-listan i din <div>.
  4. Anropa Funktionen: Anropa fetchPostAndComments(5) (eller välj ett annat id).
  5. Testa: Öppna index.html. Ser du postens titel följt av en lista med namn på de som kommenterat?

Övning 4: Felhantering med Fetch

Mål: Öva på att hantera olika typer av fel som kan uppstå vid fetch-anrop.

  1. Försök hämta från en ogiltig URL:
    • Skapa en async function.
    • Försök att fetch från en URL som inte finns, t.ex. https://jsonplaceholder.typicode.com/nonexistent-endpoint.
    • Använd try...catch. Vad händer i catch-blocket? Logga felet.
    • Kontrollera också response.ok inuti try-blocket (även om du kanske inte kommer dit om URL:en är helt fel). Visa ett felmeddelande på sidan om response.ok är false eller om ett catch-fel inträffar.
  2. Försök parsa ogiltig JSON:
    • Hämta data från en URL som inte returnerar JSON, t.ex. https://google.com (detta kan ge CORS-problem, testa annars med en lokal fil eller en URL du vet returnerar HTML).
    • Försök att anropa await response.json() på svaret.
    • Se till att ditt try...catch-block fångar felet som uppstår vid JSON-parsningen. Logga felet och visa ett användarvänligt meddelande på sidan.

Övning 5 (Bonus): Aggregera Data med Reduce

Mål: Hämta alla "posts" från JSONPlaceholder och använd reduce för att räkna hur många poster varje userId har skrivit.

  1. Hämta Alla Poster:
    • Skapa en async function.
    • Hämta data från https://jsonplaceholder.typicode.com/posts.
    • Hantera response och parsa JSON.
  2. Räkna Poster per Användare:
    • Använd reduce()-metoden på post-arrayen.
    • Ditt initialValue ska vara ett tomt objekt {}.
    • I din reducer-funktion, för varje post:
      • Kontrollera om post.userId redan finns som en nyckel i ackumulator-objektet (accumulator).
      • Om det finns, öka värdet (antalet) med 1.
      • Om det inte finns, lägg till post.userId som en nyckel med värdet 1.
      • Returnera det uppdaterade ackumulator-objektet.
  3. Visa Resultatet:
    • Logga det slutliga objektet som reduce returnerar. Det bör se ut ungefär så här: { '1': 10, '2': 10, '3': 10, ... }.
    • (Valfritt) Visa denna information på webbsidan på ett snyggt sätt.

Dessa övningar ger dig en bra grund för att arbeta med asynkron kod och externa API:er i dina framtida JavaScript-projekt!

Tekniska Intervjufrågor: Avancerad JavaScript och Asynkron Programmering

Detta avsnitt innehåller exempel på tekniska intervjufrågor som kan dyka upp gällande avancerad JavaScript, asynkron programmering och datahantering. Frågorna är utformade för att testa både teoretisk förståelse och praktisk kunskap.

Använd dessa frågor för att testa din kunskap och förbereda dig för tekniska intervjuer.


Fråga 1: Asynkron JavaScript och Event Loop

Fråga: "JavaScript är single-threaded. Förklara hur JavaScript kan hantera asynkrona operationer. Vad skrivs ut i följande kod och i vilken ordning?"

console.log("Start");

setTimeout(() => {
  console.log("Timer 1");
}, 0);

console.log("Middle");

setTimeout(() => {
  console.log("Timer 2");
}, 0);

console.log("End");

Förslag till svar: JavaScript är single-threaded men använder Event Loop:

  • Call Stack: Kör synkron kod
  • Web APIs: Hanterar asynkrona operationer (setTimeout, fetch, DOM events)
  • Callback Queue: Köar färdiga callbacks
  • Event Loop: Flyttar callbacks från queue till call stack när stack är tom

Utskrift:

Start
Middle  
End
Timer 1
Timer 2

Förklaring: Synkron kod körs först (Start, Middle, End). setTimeout callbacks, även med 0ms delay, placeras i Web APIs och sedan Callback Queue. Event Loop flyttar dem till Call Stack först när all synkron kod är klar.


Fråga 2: Callbacks och Callback Hell

Fråga: "Vad är 'Callback Hell' och hur kan det undvikas? Visa ett exempel och förklara problemen."

Förslag till svar: Callback Hell uppstår när flera asynkrona operationer behöver köras i sekvens:

// Callback Hell exempel
getData(function(a) {
  getMoreData(a, function(b) {
    getEvenMoreData(b, function(c) {
      getFinalData(c, function(d) {
        // Djupt nästlad kod...
        console.log(d);
      });
    });
  });
});

Problem:

  • Låg läsbarhet: Svårt att följa kodflödet
  • Svår felhantering: Fel måste hanteras på varje nivå
  • Svårt att underhålla: Ändringar blir komplexa

Lösningar:

  1. Promises: getData().then().then().catch()
  2. Async/Await: Gör asynkron kod synkron-liknande
  3. Namngivna funktioner: Istället för anonyma callbacks

Fråga 3: Promises och Promise States

Fråga: "Förklara Promise lifecycle. Vad är skillnaden mellan .then(), .catch() och .finally()? Skriv kod som visar alla tre."

Förslag till svar: Promise States:

  • Pending: Initial state, varken fulfilled eller rejected
  • Fulfilled: Operation lyckades, har ett värde
  • Rejected: Operation misslyckades, har en anledning (error)
  • Settled: Antingen fulfilled eller rejected (kan ej ändras)
function asyncOperation(shouldSucceed) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (shouldSucceed) {
        resolve("Success data");
      } else {
        reject(new Error("Operation failed"));
      }
    }, 1000);
  });
}

asyncOperation(true)
  .then(result => {
    console.log("Success:", result); // Körs vid fulfilled
    return result.toUpperCase();     // Kan returnera värde för kedja
  })
  .then(upperResult => {
    console.log("Processed:", upperResult);
  })
  .catch(error => {
    console.error("Error:", error.message); // Körs vid rejected
  })
  .finally(() => {
    console.log("Operation completed"); // Körs alltid
  });

Viktigt: .then() returnerar ett nytt Promise, möjliggör kedja.


Fråga 4: Async/Await vs Promises

Fråga: "Skriv om denna Promise-kod till async/await. Vad är fördelarna och nackdelarna med varje approach?"

// Promise version
function fetchUserData(userId) {
  return fetch(`/api/users/${userId}`)
    .then(response => response.json())
    .then(user => fetch(`/api/posts?userId=${user.id}`))
    .then(response => response.json())
    .then(posts => ({ user, posts }))
    .catch(error => console.error("Error:", error));
}

Förslag till svar:

// Async/Await version
async function fetchUserData(userId) {
  try {
    const userResponse = await fetch(`/api/users/${userId}`);
    const user = await userResponse.json();
    
    const postsResponse = await fetch(`/api/posts?userId=${user.id}`);
    const posts = await postsResponse.json();
    
    return { user, posts };
  } catch (error) {
    console.error("Error:", error);
  }
}

Jämförelse:

  • Async/Await fördelar: Mer läsbar, lättare att debugga, använder try/catch
  • Async/Await nackdelar: Kan vara långsammare vid parallella operationer
  • Promises fördelar: Bra för parallell execution, funktionell stil
  • Promises nackdelar: Kan bli svårläst med komplexa kedjor

Fråga 5: Fetch API och Error Handling

Fråga: "Skriv en funktion som hämtar data med Fetch API. Hantera både nätverksfel och HTTP-fel (4xx, 5xx). Varför kastar inte fetch() automatiskt fel för HTTP-fel?"

Förslag till svar:

async function safeFetch(url, options = {}) {
  try {
    const response = await fetch(url, options);
    
    // Fetch kastar INTE fel för HTTP status codes som 404, 500
    if (!response.ok) {
      // Försök läsa felmeddelande från server
      let errorMessage = `HTTP Error: ${response.status}`;
      try {
        const errorData = await response.json();
        errorMessage = errorData.message || errorMessage;
      } catch {
        // Om response inte är JSON, använd standardmeddelande
      }
      throw new Error(errorMessage);
    }
    
    return await response.json();
  } catch (error) {
    // Hantera både nätverksfel och HTTP-fel
    if (error.name === 'TypeError') {
      throw new Error("Network error - check connection");
    }
    throw error; // Re-throw HTTP errors
  }
}

// Användning
safeFetch('/api/users/123')
  .then(data => console.log('Success:', data))
  .catch(error => console.error('Failed:', error.message));

Varför fetch() inte kastar fel för HTTP-fel:

  • Fetch anser att få ett svar (även 404/500) är "success" - nätverksanropet lyckades
  • Bara nätverksfel (ingen internet, server ej tillgänglig) kastas som fel
  • response.ok kontrollerar om status är 200-299

Fråga 6: JSON och Data Transformation

Fråga: "Förklara skillnaden mellan JSON.stringify() och JSON.parse(). Vad händer med funktioner och undefined värden? Skriv kod som demonstrerar detta."

Förslag till svar:

const jsObject = {
  name: "Alice",
  age: 30,
  active: true,
  address: null,
  hobbies: ["reading", "coding"],
  greet: function() { return "Hello!"; }, // Funktion
  secret: undefined,                       // undefined
  id: Symbol('user')                      // Symbol
};

// JavaScript till JSON-sträng
const jsonString = JSON.stringify(jsObject);
console.log("JSON:", jsonString);
// Output: {"name":"Alice","age":30,"active":true,"address":null,"hobbies":["reading","coding"]}
// Notera: greet, secret och id försvinner!

// JSON-sträng till JavaScript
const parsedObject = JSON.parse(jsonString);
console.log("Parsed:", parsedObject);

// Felhantering vid parsning
try {
  const invalidJson = '{"name": "Alice", "age": 30,}'; // Avslutande komma
  JSON.parse(invalidJson);
} catch (error) {
  console.error("Parse error:", error.message);
}

// Pretty printing
const prettyJson = JSON.stringify(jsObject, null, 2);
console.log("Pretty:\n", prettyJson);

Viktiga regler:

  • Ignoreras: functions, undefined, Symbol
  • Konverteras: Date → string, NaN/Infinity → null
  • Kräver dubbla citattecken: för strings och keys
  • Felhantering: Alltid wrappa JSON.parse() i try/catch

Fråga 7: Array.map() och Transformation

Fråga: "Förklara map() metoden. Hur skiljer den sig från en for-loop? Transformera denna data för att skapa en ny struktur:"

const users = [
  { id: 1, firstName: "Anna", lastName: "Svensson", age: 25 },
  { id: 2, firstName: "Erik", lastName: "Johansson", age: 30 },
  { id: 3, firstName: "Maria", lastName: "Andersson", age: 28 }
];
// Skapa array med format: "Anna S. (25 år)"

Förslag till svar:

// Med map() - REKOMMENDERAT
const formattedUsers = users.map(user => {
  const lastInitial = user.lastName.charAt(0);
  return `${user.firstName} ${lastInitial}. (${user.age} år)`;
});

// Med traditional for-loop
const formattedUsersLoop = [];
for (let i = 0; i < users.length; i++) {
  const user = users[i];
  const lastInitial = user.lastName.charAt(0);
  formattedUsersLoop.push(`${user.firstName} ${lastInitial}. (${user.age} år)`);
}

console.log(formattedUsers);
// Output: ["Anna S. (25 år)", "Erik J. (30 år)", "Maria A. (28 år)"]

Skillnader:

  • map(): Returnerar ny array, original oförändrad, funktionell stil
  • for-loop: Imperativ, måste hantera ny array manuellt
  • map() fördelar: Kortare, mer läsbar, mindre risk för buggar
  • for-loop fördelar: Mer kontroll, kan break/continue

Viktigt: map() returnerar alltid en array med samma längd som originalet.


Fråga 8: Array.filter() och Conditional Logic

Fråga: "Använd filter() för att lösa denna uppgift: Från en array med produkter, hitta alla produkter som kostar mellan 100-500 kr OCH som finns i lager."

const products = [
  { name: "Laptop", price: 8999, inStock: true },
  { name: "Mouse", price: 299, inStock: true },
  { name: "Keyboard", price: 150, inStock: false },
  { name: "Monitor", price: 2500, inStock: true },
  { name: "Cable", price: 89, inStock: true },
  { name: "Headphones", price: 450, inStock: true }
];

Förslag till svar:

// Med filter()
const affordableInStock = products.filter(product => {
  return product.price >= 100 && 
         product.price <= 500 && 
         product.inStock === true;
});

// Kortare version
const affordableInStock2 = products.filter(p => 
  p.price >= 100 && p.price <= 500 && p.inStock
);

console.log(affordableInStock);
// Output: [
//   { name: "Mouse", price: 299, inStock: true },
//   { name: "Headphones", price: 450, inStock: true }
// ]

// Kedja filter med map för att bara få namnen
const productNames = products
  .filter(p => p.price >= 100 && p.price <= 500 && p.inStock)
  .map(p => p.name);

console.log(productNames); // ["Mouse", "Headphones"]

Viktigt:

  • filter() returnerar ny array med element som passar villkoret
  • Callback-funktionen måste returnera true/false
  • Kan kedjas med andra array-metoder

Fråga 9: Array.reduce() och Aggregation

Fråga: "Använd reduce() för att beräkna totala lagervärdet från denna produktdata. Förklara varje del av reduce-anropet."

const inventory = [
  { name: "Laptop", price: 8999, quantity: 5 },
  { name: "Mouse", price: 299, quantity: 20 },
  { name: "Monitor", price: 2500, quantity: 8 }
];

Förslag till svar:

const totalValue = inventory.reduce((accumulator, product) => {
  const productValue = product.price * product.quantity;
  console.log(`${product.name}: ${productValue} kr (Total så far: ${accumulator + productValue})`);
  return accumulator + productValue;
}, 0);

console.log("Totalt lagervärde:", totalValue, "kr");

// Output:
// Laptop: 44995 kr (Total så far: 44995)
// Mouse: 5980 kr (Total så far: 50975)  
// Monitor: 20000 kr (Total så far: 70975)
// Totalt lagervärde: 70975 kr

Reduce-delar förklarade:

  • Callback-funktion: (accumulator, product) => { ... }
    • accumulator: Ackumulerat värde från tidigare iterationer
    • product: Nuvarande element som bearbetas
  • Initial värde: 0 - startvärdet för accumulator
  • Return: Måste returnera uppdaterat accumulator-värde

Annan reduce-användning - gruppering:

// Gruppera produkter efter prisklass
const priceGroups = inventory.reduce((groups, product) => {
  const key = product.price > 1000 ? 'expensive' : 'affordable';
  if (!groups[key]) groups[key] = [];
  groups[key].push(product);
  return groups;
}, {});

Fråga 10: Destructuring och Spread Operator

Fråga: "Förklara destructuring och spread operator. Skriv kod som visar båda för arrays och objekt."

Förslag till svar:

// Array Destructuring
const coordinates = [10, 20, 30];
const [x, y, z] = coordinates;
console.log(x, y, z); // 10 20 30

// Med rest operator
const numbers = [1, 2, 3, 4, 5];
const [first, second, ...rest] = numbers;
console.log(first);  // 1
console.log(second); // 2  
console.log(rest);   // [3, 4, 5]

// Object Destructuring
const user = {
  id: 1,
  name: "Alice", 
  email: "alice@example.com",
  settings: { theme: "dark", notifications: true }
};

const { name, email } = user;
console.log(name, email); // Alice alice@example.com

// Rename och nested destructuring
const { id: userId, settings: { theme } } = user;
console.log(userId, theme); // 1 dark

// Spread Operator - Arrays
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const combined = [...arr1, 0, ...arr2];
console.log(combined); // [1, 2, 3, 0, 4, 5, 6]

// Spread Operator - Objects
const basicInfo = { name: "Bob", age: 25 };
const extendedInfo = { 
  ...basicInfo, 
  email: "bob@example.com",
  age: 26  // Överskriver age från basicInfo
};
console.log(extendedInfo);
// { name: "Bob", age: 26, email: "bob@example.com" }

// Function parameters
function greetUser({ name, age = 'unknown' }) {
  console.log(`Hello ${name}, age: ${age}`);
}
greetUser(user); // Hello Alice, age: unknown

Användningsfall:

  • Destructuring: Clean parameter extraction, multiple returns
  • Spread: Copying arrays/objects, combining data, function arguments

Fråga 11: Parallell vs Sekventiell Asynkron Kod

Fråga: "Förklara skillnaden mellan dessa två kod-exempel. När skulle du använda vilken approach?"

// Version A
async function fetchDataA() {
  const user = await fetch('/api/user').then(r => r.json());
  const posts = await fetch('/api/posts').then(r => r.json());
  const comments = await fetch('/api/comments').then(r => r.json());
  return { user, posts, comments };
}

// Version B  
async function fetchDataB() {
  const [user, posts, comments] = await Promise.all([
    fetch('/api/user').then(r => r.json()),
    fetch('/api/posts').then(r => r.json()),
    fetch('/api/comments').then(r => r.json())
  ]);
  return { user, posts, comments };
}

Förslag till svar: Version A - Sekventiell:

  • Kör requests en efter en
  • Total tid: Sum av alla requests
  • Används när requests beror på varandra

Version B - Parallell:

  • Kör requests samtidigt
  • Total tid: Längsta request
  • Används när requests är oberoende
// Tidsexempel (om varje request tar 1s):
// Version A: 3 sekunder total
// Version B: 1 sekund total

// Promise.all() alternativ och felhantering
async function robustFetch() {
  try {
    // Promise.all - misslyckas om EN request misslyckas
    const results = await Promise.all([...]);
    
    // Promise.allSettled - fortsätter även om några misslyckas
    const results2 = await Promise.allSettled([...]);
    results2.forEach((result, index) => {
      if (result.status === 'fulfilled') {
        console.log(`Request ${index}:`, result.value);
      } else {
        console.error(`Request ${index} failed:`, result.reason);
      }
    });
    
  } catch (error) {
    console.error("At least one request failed:", error);
  }
}

Välj baserat på:

  • Sekventiell: När data behövs från tidigare request
  • Parallell: När requests är oberoende, för bättre prestanda

Fråga 12: Praktisk Debugging och Felhantering

Fråga: "Du får detta fel i konsolen: 'TypeError: Cannot read property 'map' of undefined'. Hur skulle du debugga och fixa problemet i denna kod?"

async function displayUsers() {
  try {
    const response = await fetch('/api/users');
    const data = await response.json();
    
    const userList = data.users.map(user => `<li>${user.name}</li>`);
    document.getElementById('user-list').innerHTML = userList.join('');
  } catch (error) {
    console.error("Error:", error);
  }
}

Förslag till svar: Problemanalys:

  • data.users är undefined när .map() anropas
  • API:et kanske returnerar olika struktur än förväntat
  • Nätverksfel kan ge oväntad response

Debug-teknik:

async function displayUsers() {
  try {
    const response = await fetch('/api/users');
    
    // Debug 1: Kontrollera response status
    console.log('Response status:', response.status);
    
    if (!response.ok) {
      throw new Error(`HTTP Error: ${response.status}`);
    }
    
    const data = await response.json();
    
    // Debug 2: Logga faktisk data-struktur  
    console.log('Received data:', data);
    console.log('Data type:', typeof data);
    console.log('Has users property:', 'users' in data);
    
    // Defensive programming - kontrollera innan map
    if (!data || !Array.isArray(data.users)) {
      console.warn('Unexpected data structure:', data);
      // Fallback för olika API-strukturer
      const users = Array.isArray(data) ? data : [];
      return displayUsersFallback(users);
    }
    
    const userList = data.users.map(user => {
      // Debug 3: Kontrollera varje user-objekt
      if (!user || !user.name) {
        console.warn('Invalid user object:', user);
        return '<li>Unknown user</li>';
      }
      return `<li>${user.name}</li>`;
    });
    
    const listElement = document.getElementById('user-list');
    if (listElement) {
      listElement.innerHTML = userList.join('');
    } else {
      console.error('Element with id "user-list" not found');
    }
    
  } catch (error) {
    console.error("Error in displayUsers:", error);
    
    // Visa user-friendly meddelande
    const listElement = document.getElementById('user-list');
    if (listElement) {
      listElement.innerHTML = '<li>Failed to load users</li>';
    }
  }
}

function displayUsersFallback(users) {
  // Hantera alternativ data-struktur
  console.log('Using fallback for users:', users);
}

Best Practices:

  • Logga intermediate steps för att förstå data-flöde
  • Defensive programming - kontrollera data-typer
  • Graceful degradation - visa fallback vid fel
  • User-friendly error messages - inte bara console.error

Tips för Tekniska Intervjuer

  • Tänk högt - förklara ditt approach innan du kodar
  • Börja enkelt - implementera basic funktionalitet först
  • Hantera edge cases - vad händer med fel, null, undefined?
  • Diskutera performance - när är olika metoder lämpliga?
  • Visa modern syntax - men förklara varför du väljer det
  • Fråga om krav - behövs felhantering? Browser-support?

Hosting, CMS och WordPress

Detta kapitel täcker hosting-lösningar, innehållshanteringssystem och WordPress.

Webbservrar och hosting

  • Olika typer av webbservrar
  • Hosting-alternativ
  • Server-konfiguration
  • SSL/TLS och säkerhet

Hantera en virtuell privat server

  • VPS setup
  • SSH och fjärråtkomst
  • Server-administration
  • Säkerhetsåtgärder

Domänhantering och DNS

  • Domänregistrering
  • DNS-inställningar
  • A-records och CNAME
  • SSL-certifikat

Introduktion till CMS

  • Vad är ett CMS?
  • Olika CMS-alternativ
  • Headless CMS
  • CMS-arkitektur

WordPress

  • Installation och konfiguration
  • Teman och plugins
  • Innehållshantering
  • Säkerhet och underhåll

Static Site Generators med Jekyll

  • Jekyll grunderna
  • Markdown och front matter
  • Themes och layouts
  • GitHub Pages deployment

Praktiska övningar och projekt

  • WordPress-site setup
  • Custom theme development
  • Jekyll blog creation
  • Server deployment

Teknisk Intervju

Fullstack-utveckling med PHP och MySQL

Detta kapitel introducerar fullstack-utveckling med PHP och MySQL för att bygga kompletta webbapplikationer.

Introduktion till PHP

PHP (Hypertext Preprocessor) är ett populärt skriptspråk med öppen källkod som främst används för webbutveckling på serversidan. Det skapades ursprungligen av Rasmus Lerdorf 1994 och har sedan dess utvecklats till ett kraftfullt och flexibelt språk som driver en stor del av webben, inklusive välkända plattformar som WordPress och Facebook.

PHP i modern webbutveckling

Trots framväxten av nyare teknologier förblir PHP ett relevant och ofta använt språk inom webbutveckling. Några anledningar till detta är:

  • Stor community och ekosystem: Det finns ett enormt bibliotek av ramverk (t.ex. Laravel, Symfony), paket och verktyg tillgängliga för PHP.
  • Enkelhet och snabb inlärningskurva: Många utvecklare anser att PHP är relativt lätt att komma igång med jämfört med andra serverspråk.
  • Integration med databaser: PHP har utmärkt stöd för att interagera med olika databaser, särskilt MySQL.
  • Kostnadseffektivt: PHP är gratis att använda och många webbhotell erbjuder prisvärda PHP-hostingalternativ.

Utvecklingsmiljö setup

För att utveckla med PHP lokalt behöver du installera:

  1. PHP-tolken: Själva kärnan i PHP som exekverar din kod. Den kan laddas ner från php.net.
  2. En webbserver: T.ex. Apache eller Nginx, som hanterar HTTP-förfrågningar och serverar dina PHP-filer.
  3. En databas: Ofta MySQL eller MariaDB om din applikation behöver lagra data.

Ett enkelt sätt att få allt detta är att installera ett paket som XAMPP, MAMP eller WAMP, som buntar ihop Apache, MySQL och PHP (och ofta Perl/Python) för enkel installation på Windows, macOS eller Linux.

Alternativt kan du använda verktyg som Docker för att skapa isolerade och konfigurerbara utvecklingsmiljöer.

PHP och webbservrar

När en användare begär en PHP-sida från en webbserver händer följande:

  1. Webbservern (t.ex. Apache) tar emot förfrågan.
  2. Om filen har ändelsen .php, skickar servern filen till PHP-tolken.
  3. PHP-tolken exekverar koden i filen. Detta kan innebära att hämta data från en databas, bearbeta formulärdata, eller utföra annan logik.
  4. Resultatet av PHP-skriptet (vanligtvis HTML, men kan vara JSON, XML etc.) skickas tillbaka till webbservern.
  5. Webbservern skickar det slutliga resultatet (HTML) till användarens webbläsare.
sequenceDiagram
    participant B as Webbläsare
    participant WS as Webbserver (t.ex. Apache)
    participant PHP as PHP-Tolk
    participant DB as Databas (Valfritt)

    B->>+WS: Begär PHP-sida (t.ex. GET /sida.php)
    WS->>+PHP: Överlämnar sida.php för bearbetning
    alt Databasinteraktion
        PHP->>+DB: Skickar databasfråga
        DB-->>-PHP: Returnerar resultat
    end
    PHP-->>-WS: Returnerar genererad HTML/Output
    WS-->>-B: Skickar HTML-svar

Grundläggande koncept och syntax

PHP-kod bäddas oftast in direkt i HTML-dokument med hjälp av speciella taggar: <?php och ?>.

<!DOCTYPE html>
<html>
<head>
    <title>Min första PHP-sida</title>
</head>
<body>
    <h1>Välkommen!</h1>
    <p>Dagens datum är <?php echo date('Y-m-d'); ?>.</p> 
    
    <?php
        // Detta är en PHP-kommentar
        $greeting = "Hej från PHP!";
        echo "<p>" . $greeting . "</p>"; 

        $num1 = 5;
        $num2 = 10;
        $sum = $num1 + $num2;
        echo "<p>Summan av $num1 och $num2 är: $sum</p>";
    ?>
</body>
</html>

I exemplet ovan:

  • <?php ... ?>: Taggarna som omger PHP-koden.
  • echo: Används för att skriva ut data till HTML-outputen.
  • date('Y-m-d'): En inbyggd PHP-funktion som returnerar dagens datum.
  • $greeting, $num1, $num2, $sum: Variabler (börjar alltid med $ i PHP).
  • .: Konkateneringsoperatorn för att slå ihop strängar.
  • //: En enkelradskommentar i PHP.

PHP vs. JavaScript

Det är viktigt att förstå skillnaden mellan PHP och JavaScript, två vanliga språk inom webbutveckling:

  • PHP (Server-side):

    • Körs på webbservern.
    • Används för att hantera data, interagera med databaser, hantera användarsessioner, och generera dynamiskt HTML innan det skickas till webbläsaren.
    • Användaren ser aldrig själva PHP-koden, bara resultatet (oftast HTML).
    • Bra för backend-logik, databashantering, säkerhetskritiska operationer.
  • JavaScript (Client-side):

    • Körs i användarens webbläsare.
    • Används för att göra webbsidor interaktiva, manipulera HTML och CSS (DOM-manipulation), göra asynkrona anrop (t.ex. med Fetch API) och validera formulärdata utan att behöva ladda om sidan.
    • Användaren kan se JavaScript-koden (oftast).
    • Bra för användargränssnitt, interaktivitet, realtidsuppdateringar på klientsidan.

Analogi: Tänk på en restaurang. PHP är som kocken som förbereder maten (datan) i köket (servern) baserat på beställningen (HTTP-förfrågan). JavaScript är som servitören som interagerar med gästen (användaren) vid bordet (webbläsaren), tar emot specifika önskemål (användarinteraktioner) och justerar presentationen (webbsidan) direkt.

Även om JavaScript ursprungligen var ett klientspråk, kan det idag även köras på serversidan med plattformar som Node.js (vilket tas upp i kapitel 9). Men när man pratar om traditionell webbutveckling är PHP ett serverspråk och JavaScript (i webbläsaren) ett klientspråk. PHP och JavaScript kompletterar ofta varandra för att skapa rika och dynamiska webbapplikationer.

Kom igång med PHP-utveckling

För att bygga och köra PHP-applikationer lokalt behöver du en utvecklingsmiljö som inkluderar PHP-tolken, en webbserver och ofta en databas. En traditionell "stack" (samling mjukvara) för detta är LAMP (Linux, Apache, MySQL, PHP) eller LEMP (Linux, Nginx, MySQL, PHP). Istället för att installera dessa tjänster direkt på ditt operativsystem, kommer vi i den här boken att använda Docker.

Docker låter oss definiera och köra applikationer i isolerade "containrar". Detta gör det enklare att:

  • Sätta upp en konsekvent utvecklingsmiljö oavsett ditt operativsystem.
  • Hantera olika versioner av mjukvara (PHP, MySQL etc.) för olika projekt.
  • Simulera produktionsmiljön mer exakt.
  • Dela miljökonfigurationen med andra utvecklare.

Vi kommer att använda en docker-compose.yml-fil för att definiera de tjänster (services) som vår PHP-applikation behöver. I vårt fall är det:

  1. Webbserver (Apache): Apache är en mycket populär och väletablerad webbserver som ansvarar för att ta emot HTTP-förfrågningar från webbläsare och skicka dem vidare till PHP-tolken för bearbetning av .php-filer. Den serverar också statiska filer som HTML, CSS och JavaScript.
  2. Databas (MariaDB): MariaDB är en populär relationsdatabas med öppen källkod, skapad av de ursprungliga utvecklarna av MySQL. Den används för att lagra och hämta applikationens data på ett strukturerat sätt. PHP har utmärkt stöd för att kommunicera med MariaDB/MySQL.
  3. Databashanterare (phpMyAdmin): phpMyAdmin är ett webbaserat verktyg skrivet i PHP som ger ett grafiskt gränssnitt för att administrera MariaDB/MySQL-databaser. Det är praktiskt för att titta på data, köra SQL-frågor manuellt och hantera databasstrukturen under utveckling.
  4. PHP: Själva PHP-tjänsten som kör vår applikationskod.

Nedan följer ett exempel på hur en docker-compose.yml-fil kan se ut för att sätta upp denna miljö. (Detaljer kring Docker och Docker Compose täcks mer ingående i andra sammanhang, men detta ger en grundläggande uppfattning).


docker-compose.yml

services:
    php:
        build:
            context: .
            dockerfile: Dockerfile
        volumes:
            - ./app/public:/var/www/html
        ports:
            - 8060:80
    mysql:
        image: mariadb:latest
        environment:
            MYSQL_ROOT_PASSWORD: db_root_password
            MYSQL_USER: db_user
            MYSQL_PASSWORD: db_password
            MYSQL_DATABASE: db_fullstack
        volumes:
            - mysqldata:/var/lib/mysql
        ports:
            - 38060:3306
    phpmyadmin:
        image: phpmyadmin
        restart: always
        ports:
            - 8061:80
        environment:
            - PMA_ARBITRARY=1
        depends_on:
            - mysql
volumes:
    mysqldata: {}

Dockerfile

FROM php:8-apache
RUN a2enmod ssl && a2enmod rewrite
RUN service apache2 restart
RUN apt-get update && apt-get install -y
RUN docker-php-ext-install mysqli pdo pdo_mysql

Mappstruktur för exemplet ovan

project
├───app
│   └───public
│       └───index.php
├───Dockerfile
├───docker-compose.yml

Demo: Server-side vs Client-side

För att illustrera skillnaden mellan kod som körs på servern (PHP) och kod som körs i klienten (JavaScript), kan vi skapa en enkel index.php-fil:

<!DOCTYPE html>
<html>
<head>
    <title>PHP & JS Demo</title>
</head>
<body>
    <h1>PHP säger:</h1>
    <?php
        // Denna PHP-kod körs på servern
        $serverMessage = "Jag körs på servern!";
        echo "<p>" . htmlspecialchars($serverMessage) . "</p>"; 
    ?>

    <script>
        // Denna JavaScript-kod körs i klientens webbläsare
        console.log("Jag körs på klienten (i webbläsarens konsol)!");
        alert("Hej från JavaScript!"); // Visar en popup i webbläsaren
    </script>
</body>
</html>

När du besöker denna sida i webbläsaren:

  1. PHP-koden (echo ...) exekveras på servern och genererar HTML-texten <p>Jag körs på servern!</p>.
  2. Den kompletta HTML-koden (inklusive <script>-taggarna) skickas till webbläsaren.
  3. Webbläsaren renderar HTML:en och exekverar JavaScript-koden inuti <script>-taggarna, vilket skriver ut meddelandet till webbläsarkonsolen och visar en alert-ruta.

Detta visar tydligt hur PHP genererar innehållet på serversidan medan JavaScript hanterar interaktivitet och logik på klientsidan.

Introduktion till PHP

  • PHP i modern webbutveckling
  • Utvecklingsmiljö setup
  • PHP och webbservrar
  • Grundläggande koncept

PHP Syntax och grundläggande funktioner

Detta avsnitt dyker djupare in i den grundläggande syntaxen för PHP. Om du har erfarenhet av JavaScript kommer mycket att kännas bekant, men det finns viktiga skillnader i hur du skriver variabler, hanterar datatyper, och strukturerar din kod. Vi kommer att fokusera på dessa skillnader och även introducera några moderna PHP-funktioner.

PHP-taggar och kommentarer

PHP-kod exekveras på servern och bäddas oftast in i HTML-dokument med hjälp av speciella taggar.

  • Standardtaggar: <?php ... ?> Detta är det vanligaste och mest rekommenderade sättet att markera PHP-kodblock.
  • Kort echo-tagg: <?= ... ?> En genväg för <?php echo ... ?>. Användbar för att snabbt skriva ut en variabel eller resultatet av en funktion direkt i HTML.
<!DOCTYPE html>
<html>
<head>
    <title>PHP Taggar</title>
</head>
<body>
    <h1>Välkommen</h1>
    <p>Dagens datum är: <?= date('Y-m-d'); ?></p> 

    <?php
        // Detta är en PHP-kommentar (enkelrad)
        $message = "Hej från ett PHP-block!";
        /*
           Detta är en
           flerradskommentar i PHP.
        */
        echo "<p>" . $message . "</p>"; 
    ?>
</body>
</html>
  • Kommentarer: Liknar JavaScript: // för enkelradskommentarer och /* ... */ för flerradskommentarer.

Kodstandard och namngivningsprinciper

Att följa en kodstandard är något som programmerare bör vara insatt i. PHP är ett programspråk som funnits under relativt lång tid och därmed förändras också en kodstandard. Sträva efter att vara konsekvent.

Variabler

En av de mest uppenbara skillnaderna från JavaScript är hur variabler deklareras och används i PHP.

  • Alla variabelnamn i PHP inleds med ett dollartecken ($).
  • Efter $ måste namnet börja med en bokstav eller ett understreck (_), följt av valfritt antal bokstäver, siffror eller understreck.
  • PHP är Case sensitive för variabelnamn ($name och $Name är två olika variabler). En variabel namnges med gemener. Använd understreck mellan ord.
$last_name
$is_authenticated

Funktioner

Funktionsnamn skrivs med gemener och understreck mellan ord - snake_case.

get_name()
render_copyright()

Klasser och metoder

En klass namnges PascalCase, och metoder i klassen som camelCase.

MyClass() {
    function __construct() {
    
    }
    
    public function printInfo() {

    }
}

PHP:

<?php
$first_name = "Anna"; // Skapar och tilldelar en sträng
$age = 30;          // Skapar och tilldelar ett heltal
$price = 199.50;    // Skapar och tilldelar ett flyttal
$is_valid = true;   // Namn kan börja med _

$age = 31; // Ändrar värdet på en befintlig variabel

echo "Namn: " . $first_name . ", Ålder: " . $age;
?>

JavaScript (jämförelse):

let firstName = "Anna"; // Kräver let, const (eller var)
let age = 30;
const price = 199.50; // Konstant, kan inte ändras
let isValid = true;   // Inget $ behövs

age = 31; // Ändra värde

console.log(`Namn: ${firstName}, Ålder: ${age}`); // Template literals för enkel utskrift

Datatyper

PHP har flera inbyggda datatyper. De viktigaste kan delas in i:

Skalära Typer (Scalar Types)

Dessa representerar enskilda värden.

  • int (Integer): Heltal (t.ex. 10, -5, 0).
  • float (Floating-point number, även double): Decimaltal (t.ex. 3.14, -0.5, 1.0).
  • string: Textsträngar. Kan omges av enkla ('...') eller dubbla ("...") citationstecken.
    • Dubbla citationstecken (") tillåter variabelinterpolering (variabler inuti strängen ersätts med deras värden) och specialtecken som \n (nyrad), \t (tab).
    • Enkla citationstecken (') behandlar nästan all text bokstavligt (literal), ingen variabelinterpolering sker (förutom \\ och \'). De är ofta snabbare när ingen interpolering behövs.
  • bool (Boolean): Logiska värden, antingen true eller false (skiftlägesokänsliga).
<?php
$count = 100;       // int
$pi = 3.14159;    // float
$is_admin = false;   // bool

$single_quote = 'Detta är en sträng. Variabel: $count \n'; // $count och \n skrivs ut bokstavligt
$double_quote = "Detta är en sträng. Variabel: $count \n"; // $count ersätts, \n blir nyrad

echo $single_quote; 
// Output: Detta är en sträng. Variabel: $count \n

echo $double_quote;
// Output: Detta är en sträng. Variabel: 100 
// (och en nyrad)
?>

Sammansatta Typer (Compound Types)

Dessa kan innehålla flera värden.

  • array: Ordnad mappning av nyckel-värde-par. Kan vara indexerad (numeriska nycklar) eller associativ (strängnycklar). (Se array-loopar.md för detaljer).
  • object: Instanser av klasser.
  • callable: En referens till en funktion.

Speciella Typer

  • resource: En speciell variabel som håller en referens till en extern resurs (t.ex. en databasanslutning, en öppen fil).
  • null: Representerar avsaknaden av ett värde. En variabel har värdet null om den har tilldelats null, inte har tilldelats något värde alls, eller har blivit unset().

PHP är ett dynamiskt typat språk (som JavaScript) som standard, vilket innebär att du inte behöver specificera datatypen när du skapar en variabel. PHP avgör typen baserat på värdet den tilldelas, och typen kan ändras under körningens gång (även om detta bör undvikas om möjligt).

<?php
$value = 10;     // $value är nu int
echo gettype($value); // Output: integer

$value = "Hej";  // $value är nu string
echo gettype($value); // Output: string
?>

(Vi kommer till Type Hinting senare, vilket låter dig vara mer explicit med typer i funktioner.)

Konstanter

För värden som inte ska ändras under skriptets körning kan du definiera konstanter med define() eller const.

  • define(name, value, case_insensitive): Traditionellt sätt. Namnet är en sträng. Standard är skiftlägeskänsligt.
  • const NAME = value;: Nyare sätt (sedan PHP 5.3), används oftast inom klasser men fungerar även globalt. Kan inte användas inom kontrollstrukturer (som if). Alltid skiftlägeskänsligt.

Konstanter har inget $ framför sig.

<?php
define("APP_VERSION", "1.0.2");
const DEFAULT_LANG = "sv";

echo APP_VERSION; // Output: 1.0.2
echo DEFAULT_LANG; // Output: sv

// Försök att ändra konstant (ger fel)
// APP_VERSION = "1.1.0"; // Fatal error
// const DEFAULT_LANG = "en"; // Parse error
?>

Operatorer

PHP har många operatorer som liknar de i JavaScript.

  • Aritmetiska: +, -, *, /, % (modulo/rest), ** (exponent, sedan PHP 5.6).
  • Tilldelning: =, +=, -=, *=, /=, %=, **=, .= (för strängkonkatenering).
  • Jämförelse:
    • == (Lika, efter typomvandling - undvik ofta!)
    • === (Identisk, samma värde OCH samma typ - rekommenderas!)
    • != eller <> (Inte lika, efter typomvandling)
    • !== (Inte identisk, olika värde ELLER olika typ - rekommenderas!)
    • <, >, <=, >=
    • <=> (Spaceship operator, sedan PHP 7): Returnerar -1, 0, eller 1 beroende på om vänster är mindre än, lika med, eller större än höger.
  • Logiska: && (and), || (or), ! (not), and (samma som && men lägre prioritet), or (samma som || men lägre prioritet), xor.
  • Strängkonkatenering: . (punkt) används för att slå ihop strängar.
  • Felkontroll: @ (används framför ett uttryck för att tysta eventuella fel/varningar det genererar - använd med extrem försiktighet!)
  • Null Coalescing: ?? (sedan PHP 7): Returnerar vänster operand om den existerar och inte är null, annars höger operand. Bra för att sätta defaultvärden.
  • Null Coalescing Assignment: ??= (sedan PHP 7.4): Tilldelar höger operand till vänster operand endast om vänster operand är null.
<?php
$a = 5;
$b = "5";
$c = 10;

var_dump($a == $b);  // bool(true) - Undvik! '5' görs om till 5.
var_dump($a === $b); // bool(false) - Rekommenderas! Olika typer.
var_dump($a != $b);  // bool(false)
var_dump($a !== $b); // bool(true) - Rekommenderas!

var_dump($a <=> $c); // int(-1)
var_dump($a <=> $a); // int(0)
var_dump($c <=> $a); // int(1)

$name = $_GET['user'] ?? 'Gäst'; // Om $_GET['user'] inte finns eller är null, blir $name 'Gäst'
echo "Välkommen, $name!\n";

$settings['theme'] = $settings['theme'] ?? 'light'; // Traditionellt
$settings['theme'] ??= 'light'; // Samma som ovan med ??=

$greeting = "Hej";
$target = "Världen";
$fullGreeting = $greeting . " " . $target . "!"; // Strängkonkatenering
echo $fullGreeting; // Output: Hej Världen!
?>

Kontrollstrukturer

Styr hur koden exekveras.

if, elseif, else

Fungerar i princip identiskt med JavaScript.

<?php
$score = 75;

if ($score >= 90) {
    echo "Betyg: A";
} elseif ($score >= 80) {
    echo "Betyg: B";
} elseif ($score >= 65) {
    echo "Betyg: C"; // Denna körs
} else {
    echo "Betyg: F";
}
?>

switch

Liknar JavaScripts switch, men använder lös jämförelse (==) som standard. Kom ihåg break;!

<?php
$day = "Måndag";

switch ($day) {
    case "Måndag":
        echo "Start på veckan.";
        break;
    case "Fredag":
        echo "Snart helg!";
        break;
    default:
        echo "En vanlig dag.";
        // break; behövs inte i default
}
?>

match (PHP 8+)

Ett modernare alternativ till switch. Viktiga skillnader:

  • Använder strikt jämförelse (===). Mycket säkrare!
  • Är ett uttryck, vilket betyder att det returnerar ett värde.
  • Har inga break, faller inte igenom.
  • Måste vara uttömmande (ha ett default-fall eller täcka alla möjliga värden om input-typen är känd, t.ex. en enum).
  • Kan ha flera värden per arm, separerade med komma.
<?php
$httpStatusCode = 200;

$message = match ($httpStatusCode) {
    200, 201, 204 => "Success!",
    400 => "Bad Request",
    404 => "Not Found",
    500 => "Server Error",
    default => "Unknown status code",
};

echo $message; // Output: Success!

// Jämför med switch:
// switch ($httpStatusCode) {
//     case 200:
//     case 201:
//     case 204:
//         $message = "Success!";
//         break;
//     case 400:
//         $message = "Bad Request";
//         break;
//     // ... etc ...
// }
?>

match är ofta att föredra framför switch i modern PHP tack vare strikt jämförelse och att det är ett uttryck.

Loopar (for, while, do-while, foreach)

for, while, och do-while fungerar i stort sett som i JavaScript. foreach är specifikt designad för att loopa över arrayer och objekt och täcks i detalj i array-loopar.md.

<?php
// for-loop
for ($i = 0; $i < 5; $i++) {
    echo "i är $i\n";
}

// while-loop
$j = 0;
while ($j < 3) {
    echo "j är $j\n";
    $j++;
}

// do-while loop (körs minst en gång)
$k = 5;
do {
    echo "k är $k\n"; // Körs en gång
    $k++;
} while ($k < 5);
?>

Funktioner

Att definiera och anropa funktioner liknar JavaScript, men med PHP-syntax.

<?php
// Definiera en funktion
function greet($name) {
    echo "Hej, " . $name . "!";
}

// Anropa funktionen
greet("Anna"); // Output: Hej, Anna!

// Funktion som returnerar ett värde
function add($num1, $num2) {
    return $num1 + $num2;
}

$sum = add(10, 5);
echo "\nSumman är: " . $sum; // Output: Summan är: 15
?>

Type Hinting (Typdeklarationer)

Sedan PHP 7 har möjligheten att deklarera förväntade typer för funktionsparametrar och returvärden förbättrats avsevärt. Detta kallas type hinting eller typdeklarationer.

  • Parameter Types: Ange förväntad typ före parameternamnet.
  • Return Types: Ange förväntad returtyp efter parameterlistan med ett kolon (:).
  • Nullable Types: Om en parameter eller returvärde kan vara antingen den specificerade typen eller null, sätt ett frågetecken (?) före typen (t.ex. ?string).
  • Union Types (PHP 8+): Tillåter att en parameter eller returvärde kan vara en av flera typer, separerade med | (t.ex. int|float).
  • mixed Type (PHP 8+): Indikerar att en parameter eller returvärde kan vara av vilken typ som helst.
  • void Return Type (PHP 7.1+): Indikerar att en funktion inte returnerar något värde.

Om en funktion anropas med fel typ eller returnerar fel typ (och strikta typer är aktiverade, se nedan), kastas ett TypeError.

<?php
// Aktivera strikta typer (rekommenderas i början av filer)
declare(strict_types=1);

// Funktion med typdeklarationer
function calculateArea(float $width, float $height): float {
    if ($width <= 0 || $height <= 0) {
        // Kanske kasta ett undantag istället
        throw new InvalidArgumentException("Bredd och höjd måste vara större än 0");
    }
    return $width * $height;
}

$area = calculateArea(10.5, 5.2);
echo "Area: " . $area . "\n"; // Output: Area: 54.6

// $invalidArea = calculateArea(-10.0, 5.0); // Detta ger ett TypeError pga -10.0

// Funktion med nullable return type
function findUser(int $id): ?array { // Kan returnera array eller null
    // ... logik för att hämta användare ...
    if ($id === 1) {
        return ['id' => 1, 'name' => 'Anna'];
    }
    return null;
}

// Funktion med union type parameter och void return type
function processValue(int|string $input): void {
    echo "Bearbetar: " . $input . "\n";
    // Returnerar inget
}

processValue(123);
processValue("abc");
?>

declare(strict_types=1);: Denna deklaration, när den placeras i början av en PHP-fil, aktiverar strikt typläge. I strikt läge accepteras endast värden av exakt den typ som deklarerats (med några få undantag som int till float). Utan strikt läge (default), försöker PHP tvångsomvandla (coerce) värden till den förväntade typen om möjligt (t.ex. strängen "10" skulle accepteras för en int-parameter). Att använda strikta typer rekommenderas starkt för att fånga fel tidigare och göra koden mer förutsägbar.

Att använda type hinting gör din PHP-kod mer robust, lättare att förstå, och hjälper utvecklingsverktyg (som IDE:er) att ge bättre assistans och felkontroll.

Detta täcker de mest grundläggande syntaxelementen i PHP. Nästa steg är att se hur man interagerar med databaser (sql.md), hanterar sessioner (sessions.md) och tänker på säkerhet (security.md) innan vi bygger vår CRUD-applikation.

Arrayer och Loopar i PHP

Arrayer är en fundamental datastruktur i de flesta programmeringsspråk, och PHP är inget undantag. De låter oss lagra flera värden i en enda variabel. Om du kommer från en JavaScript-bakgrund kommer du att känna igen konceptet, men det finns viktiga skillnader i syntax och funktionalitet i PHP. Detta avsnitt täcker grunderna för PHP-arrayer och hur man itererar (loopar) över dem.

Vad är en Array i PHP?

En array i PHP är en ordnad mappning (en samling) av nyckel-värde-par. Nycklarna kan antingen vara heltal (numeriska index) eller strängar. Värdena kan vara av vilken datatyp som helst, inklusive andra arrayer.

Det finns två huvudsakliga typer av arrayer i PHP:

  1. Indexerade (Numeriska) Arrayer: Arrayer med numeriska index, vanligtvis startande från 0.
  2. Associativa Arrayer: Arrayer med namngivna strängnycklar.

Indexerade Arrayer

Dessa liknar mest de "vanliga" arrayerna i JavaScript.

PHP:

<?php
// Skapa en indexerad array (äldre syntax)
$colors_old = array("Röd", "Grön", "Blå");

// Skapa en indexerad array (modern kort syntax, från PHP 5.4+)
$colors = ["Röd", "Grön", "Blå"];

// Lägg till ett element i slutet
$colors[] = "Gul"; // Index 3 tilldelas automatiskt

// Hämta ett element med dess index
echo $colors[0]; // Output: Röd
echo $colors[3]; // Output: Gul

// Ändra ett element
$colors[1] = "Limegrön"; 
echo $colors[1]; // Output: Limegrön

// Se hela arrayens struktur (bra för debugging)
print_r($colors); 
/* Output:
Array
(
    [0] => Röd
    [1] => Limegrön
    [2] => Blå
    [3] => Gul
)
*/

// Få antalet element
echo count($colors); // Output: 4
?>

JavaScript (Jämförelse):

// Skapa en array
let colors = ["Röd", "Grön", "Blå"];

// Lägg till ett element i slutet
colors.push("Gul");

// Hämta ett element med dess index
console.log(colors[0]); // Output: Röd
console.log(colors[3]); // Output: Gul

// Ändra ett element
colors[1] = "Limegrön";
console.log(colors[1]); // Output: Limegrön

// Se hela arrayen
console.log(colors); // Output: ["Röd", "Limegrön", "Blå", "Gul"]

// Få antalet element (längden)
console.log(colors.length); // Output: 4

Skillnader att notera:

  • Syntax: PHP använder [] eller array() för att skapa arrayer, JavaScript använder [].
  • Lägga till element: PHP använder $array[] = value; för att lägga till i slutet, JavaScript använder array.push(value).
  • Storlek: PHP använder funktionen count(), JavaScript använder array.length-egenskapen.

Associativa Arrayer

Detta är en av PHP:s styrkor. Associativa arrayer låter dig använda meningsfulla strängar som nycklar istället för bara siffror. De liknar JavaScript-objekt som används som dictionaries eller hashmappar.

PHP:

<?php
// Skapa en associativ array
$person = [
    "firstName" => "Anna",
    "lastName" => "Andersson",
    "age" => 30,
    "city" => "Stockholm"
];

// Hämta ett värde med dess nyckel
echo $person["firstName"]; // Output: Anna
echo $person["age"];     // Output: 30

// Lägga till ett nytt nyckel-värde-par
$person["email"] = "anna.andersson@example.com";

// Ändra ett värde
$person["city"] = "Göteborg";

// Se hela arrayens struktur
print_r($person);
/* Output:
Array
(
    [firstName] => Anna
    [lastName] => Andersson
    [age] => 30
    [city] => Göteborg
    [email] => anna.andersson@example.com
)
*/

// Kolla om en nyckel finns
if (isset($person["age"])) {
    echo "Ålder finns och är: " . $person["age"]; 
}

// Ta bort ett element
unset($person["lastName"]); 
print_r($person); // lastName är nu borta
?>

JavaScript (Jämförelse med Objekt):

// Skapa ett objekt (motsvarigheten)
let person = {
    firstName: "Anna",
    lastName: "Andersson",
    age: 30,
    city: "Stockholm"
};

// Hämta ett värde med dess nyckel (dot notation eller bracket notation)
console.log(person.firstName); // Output: Anna
console.log(person["age"]);    // Output: 30

// Lägga till en ny egenskap
person.email = "anna.andersson@example.com";

// Ändra ett värde
person.city = "Göteborg";

// Se hela objektet
console.log(person); 
/* Output:
{
  firstName: 'Anna',
  lastName: 'Andersson',
  age: 30,
  city: 'Göteborg',
  email: 'anna.andersson@example.com'
}
*/

// Kolla om en egenskap finns
if ("age" in person) { // eller person.hasOwnProperty('age')
    console.log("Ålder finns och är: " + person.age);
}

// Ta bort en egenskap
delete person.lastName;
console.log(person); // lastName är nu borta

Skillnader att notera:

  • Grundtyp: PHP använder sin array-typ för båda, JavaScript använder object för detta ändamål (även om Map är ett modernare alternativ för rena key-value-stores).
  • Syntax: PHP använder => för att associera nyckel och värde, JavaScript använder :.
  • Kolla existens: PHP använder isset(), JavaScript använder in-operatorn eller hasOwnProperty().
  • Ta bort: PHP använder unset(), JavaScript använder delete.

Loopar över Arrayer

Att iterera över elementen i en array är en mycket vanlig uppgift.

foreach-loopen (PHP)

Den mest använda och idiomatiska loopen för arrayer i PHP är foreach. Den fungerar smidigt med både indexerade och associativa arrayer.

För indexerade och associativa (endast värden):

<?php
$colors = ["Röd", "Grön", "Blå"];
foreach ($colors as $color) {
    echo $color . "<br>"; 
}
// Output:
// Röd
// Grön
// Blå

$person = ["firstName" => "Anna", "age" => 30];
foreach ($person as $value) {
    echo $value . "<br>";
}
// Output:
// Anna
// 30
?>

För associativa (nyckel och värde):

<?php
$person = [
    "firstName" => "Anna",
    "lastName" => "Andersson",
    "age" => 30
];

foreach ($person as $key => $value) {
    echo $key . ": " . $value . "<br>";
}
// Output:
// firstName: Anna
// lastName: Andersson
// age: 30
?>

for-loopen (PHP)

Kan användas för indexerade arrayer, men är mindre vanlig än foreach. Kräver att indexen är sekventiella och börjar på 0.

<?php
$colors = ["Röd", "Grön", "Blå"];
$count = count($colors); // Hämta antalet element först
for ($i = 0; $i < $count; $i++) {
    echo "Index " . $i . ": " . $colors[$i] . "<br>";
}
// Output:
// Index 0: Röd
// Index 1: Grön
// Index 2: Blå
?>

JavaScript Loopar (Jämförelse)

JavaScript erbjuder flera sätt att loopa:

for...of (för värden i itererbara objekt som Arrayer):

const colors = ["Röd", "Grön", "Blå"];
for (const color of colors) {
  console.log(color);
}
// Output:
// Röd
// Grön
// Blå

forEach (array-metod):

const colors = ["Röd", "Grön", "Blå"];
colors.forEach(function(color, index) {
  console.log(`Index ${index}: ${color}`);
});
// Output:
// Index 0: Röd
// Index 1: Grön
// Index 2: Blå

const person = { firstName: "Anna", age: 30 };
// forEach fungerar inte direkt på vanliga objekt, men på Map eller via Object.entries etc.
Object.entries(person).forEach(([key, value]) => {
  console.log(`${key}: ${value}`);
});
// Output:
// firstName: Anna
// age: 30

for...in (för nycklar/egenskaper i objekt - Används sällan för Arrayer):

const person = { firstName: "Anna", age: 30 };
for (const key in person) {
  if (person.hasOwnProperty(key)) { // Viktigt att kolla!
    console.log(`${key}: ${person[key]}`);
  }
}
// Output:
// firstName: Anna
// age: 30

for (traditionell loop):

const colors = ["Röd", "Grön", "Blå"];
for (let i = 0; i < colors.length; i++) {
  console.log(`Index ${i}: ${colors[i]}`);
}
// Output:
// Index 0: Röd
// Index 1: Grön
// Index 2: Blå

Sammanfattning PHP vs JS Loops:

  • PHP:s foreach är mycket flexibel och motsvarar närmast en kombination av JS for...of (för värden) och iteration över Object.entries() (för nyckel/värde).
  • PHP:s for är lik JS for.
  • JS forEach är en metod på array-prototypen, vilket PHP saknar (PHP använder globala funktioner eller språk-konstruktioner).
  • JS for...in är primärt för objekt-egenskaper och bör användas med försiktighet för arrayer.

Vanliga Array-funktioner i PHP

PHP har ett stort bibliotek av inbyggda funktioner för att manipulera arrayer. Här är några vanliga:

  • count($array): Returnerar antalet element i arrayen. (JS: array.length)
  • array_push($array, $value1, $value2...): Lägger till ett eller flera element i slutet av arrayen. (JS: array.push())
  • array_pop($array): Tar bort och returnerar det sista elementet från arrayen. (JS: array.pop())
  • array_unshift($array, $value1...): Lägger till ett eller flera element i början av arrayen. (JS: array.unshift())
  • array_shift($array): Tar bort och returnerar det första elementet från arrayen. (JS: array.shift())
  • in_array($needle, $haystack): Kontrollerar om ett värde ($needle) finns i arrayen ($haystack). Returnerar true eller false. (JS: array.includes())
  • array_key_exists($key, $array) / isset($array[$key]): Kontrollerar om en specifik nyckel eller index finns i arrayen. isset är vanligare och kollar även att värdet inte är null. (JS: key in object / object.hasOwnProperty(key))
  • array_keys($array): Returnerar en ny array som innehåller alla nycklar från $array. (JS: Object.keys())
  • array_values($array): Returnerar en ny array som innehåller alla värden från $array, med nya numeriska index. (JS: Object.values())
  • sort($array): Sorterar en array (värden) i stigande ordning. Modifierar originalarrayen. (JS: array.sort())
  • rsort($array): Sorterar en array i fallande ordning.
  • asort($array): Sorterar en associativ array baserat på värdena, men behåller nyckel-värde-associationerna.
  • ksort($array): Sorterar en associativ array baserat på nycklarna, och behåller associationerna.
  • array_merge($array1, $array2...): Slår ihop en eller flera arrayer till en ny array. (JS: array1.concat(array2) eller spread syntax [...array1, ...array2])
  • array_slice($array, $offset, $length): Extraherar en del av en array. (JS: array.slice())
  • array_splice($array, $offset, $length, $replacement): Tar bort och/eller ersätter en del av en array. (JS: array.splice())
  • implode($glue, $pieces): Fogar ihop elementen i en array ($pieces) till en sträng, separerade av $glue. (JS: array.join())
  • explode($delimiter, $string): Delar upp en sträng till en array baserat på en avgränsare ($delimiter). (JS: string.split())

Exempel:

<?php
$fruits = ["äpple", "banan", "päron"];
echo "Antal frukter: " . count($fruits) . "<br>"; // 3

array_push($fruits, "apelsin"); 
print_r($fruits); // Array ( [0] => äpple [1] => banan [2] => päron [3] => apelsin )
echo "<br>";

if (in_array("banan", $fruits)) {
    echo "Banan finns i arrayen<br>";
}

$person = ["namn" => "Erik", "stad" => "Malmö"];
$keys = array_keys($person);
print_r($keys); // Array ( [0] => namn [1] => stad )
echo "<br>";

$values = array_values($person);
print_r($values); // Array ( [0] => Erik [1] => Malmö )
echo "<br>";

$fruit_string = implode(", ", $fruits);
echo $fruit_string; // äpple, banan, päron, apelsin
?>

Arrayer och loopar är centrala delar av PHP-programmering, särskilt vid hantering av data från databaser eller formulär. Att förstå både indexerade och associativa arrayer samt hur foreach-loopen fungerar är avgörande.

PHP klasser

Introduktion till Klasser i PHP

I tidigare avsnitt har vi sett hur vi kan använda associativa arrayer för att representera strukturerad data, som till exempel en användare eller en produkt:

<?php
$userArray = [
    'username' => 'kalleanka',
    'email' => 'kalle@example.com',
    'lastLogin' => '2024-04-10 10:30:00'
];

echo "Användarnamn: " . $userArray['username'];
?>

Detta fungerar bra för enkla fall, men när applikationer växer stöter vi på begränsningar:

  • Ingen garanterad struktur: PHP hindrar oss inte från att stava fel på en nyckel ($userArray['emial'] = ...) eller glömma att lägga till en viktig nyckel. Det finns ingen fast "mall" för hur en användar-array ska se ut.
  • Ingen datatypvalidering: Vi kan råka lägga in en siffra där en sträng förväntas, eller tvärtom.
  • Separation av data och beteende: Om vi vill utföra operationer relaterade till användaren (t.ex. kontrollera om lösenordet är giltigt, uppdatera e-postadressen, skicka ett välkomstmail), måste vi skriva separata funktioner som tar användar-arrayen som parameter. Datan (arrayen) och logiken (funktionerna) är inte direkt kopplade.

För att lösa dessa problem introducerar PHP, likt många andra moderna språk, konceptet Object-Oriented Programming (OOP), och dess kärna är klasser (classes) och objekt (objects).

Vad är en Klass?

En klass är en mall eller ritning för att skapa objekt. Den definierar:

  • Properties (Egenskaper): Variabler som tillhör klassen och lagrar data för objekten (t.ex. $username, $email).
  • Methods (Metoder): Funktioner som tillhör klassen och definierar beteendet eller operationerna som objekten kan utföra (t.ex. login(), updateProfile()).

Vad är ett Objekt?

Ett objekt är en instans av en klass. Det är en konkret representation skapad från klassens mall, med sina egna värden för egenskaperna.

Liknelse: Tänk på en pepparkaksform (klassen GingerbreadMan). Formen definierar egenskaperna (armar, ben, huvud) och potentiella metoder (kan dekoreras). Varje enskild pepparkaka du stansar ut med formen är ett objekt – en instans av klassen GingerbreadMan. Varje pepparkaka (objekt) kan ha olika dekoration (olika värden för sina egenskaper).

Definiera en Enkel Klass

Vi använder class-nyckelordet följt av klassnamnet (ofta med stor bokstav i början, PascalCase).

<?php
declare(strict_types=1); // Bra att använda med klasser!

class User 
{
    // Properties (Egenskaper) - Variabler inuti klassen
    public string $username;
    public string $email;
    public ?DateTime $lastLogin = null; // Kan vara DateTime eller null

    // Methods (Metoder) - Funktioner inuti klassen
    public function displayGreeting(): void 
    {
        echo "Hej, " . $this->username . "!";
    }
}
?>

Förklaring:

  • class User { ... }: Definierar en klass med namnet User.
  • public string $username;: Definierar en egenskap (property) som heter username.
    • public: Detta är en visibility modifier (synlighetskontroll). public betyder att egenskapen kan nås och ändras direkt utifrån objektet.
    • string: Detta är type hinting för egenskapen (sedan PHP 7.4). Det specificerar att $username förväntas innehålla en sträng.
    • ?DateTime $lastLogin = null;: Egenskapen lastLogin kan antingen innehålla ett DateTime-objekt eller vara null. Den ges ett standardvärde null.
  • public function displayGreeting(): void { ... }: Definierar en metod (method) som heter displayGreeting.
    • public: Metoden kan anropas utifrån objektet.
    • (): void: Metoden tar inga argument och returnerar inget (void).
    • $this->username: Inuti en metod refererar den speciella variabeln $this till det aktuella objektet (den specifika instansen av klassen). Vi använder -> (objektoperatorn) för att komma åt objektets egenskaper ($username i detta fall).

Skapa och Använda Objekt

Vi skapar ett objekt (en instans) från klassen med new-nyckelordet.

<?php
// Inkludera eller definiera User-klassen här...
require_once 'User.php'; // Antag att klassen ligger i User.php

// Skapa ett nytt User-objekt (en instans av User-klassen)
$user1 = new User();

// Sätta värden på (publika) egenskaper med ->
$user1->username = 'kalleanka';
$user1->email = 'kalle@example.com';
$user1->lastLogin = new DateTime('2024-04-10 10:30:00');

// Hämta värden från egenskaper
echo "Användarnamn: " . $user1->username . "\n"; // Output: Användarnamn: kalleanka

// Anropa en metod på objektet
$user1->displayGreeting(); // Output: Hej, kalleanka!

echo "\n";

// Skapa ett annat User-objekt
$user2 = new User();
$user2->username = 'mussepigg';
$user2->email = 'musse@example.com';

$user2->displayGreeting(); // Output: Hej, mussepigg!

echo "\nAnvändare 1 E-post: " . $user1->email; // Output: Användare 1 E-post: kalle@example.com
echo "\nAnvändare 2 E-post: " . $user2->email; // Output: Användare 2 E-post: musse@example.com
?>

Vi ser här att $user1 och $user2 är separata objekt, även om de är skapade från samma klass. De har sina egna kopior av egenskaperna.

Konstruktorer (__construct)

Det är ofta opraktiskt att behöva sätta alla egenskaper manuellt efter att objektet har skapats. En konstruktor är en speciell metod som anropas automatiskt när ett nytt objekt skapas med new.

Konstruktorn heter alltid __construct (två understreck).

<?php
declare(strict_types=1);

class Product
{
    public string $name;
    public float $price;
    public int $stock;

    // Konstruktor-metod
    public function __construct(string $productName, float $productPrice, int $initialStock = 0)
    {
        echo "-- Skapar nytt Product objekt --\n";
        $this->name = $productName;
        $this->price = $productPrice;
        $this->stock = $initialStock;
    }

    public function displayInfo(): void
    {
        echo "Produkt: {$this->name}, Pris: {$this->price} kr, Lager: {$this->stock} st\n";
    }
}

// Skapa objekt och skicka med argument till konstruktorn
$product1 = new Product('Tangentbord', 499.0, 50);
$product2 = new Product('Mus', 249.50); // Använder defaultvärdet 0 för $initialStock

$product1->displayInfo(); // Output: Produkt: Tangentbord, Pris: 499 kr, Lager: 50 st
$product2->displayInfo(); // Output: Produkt: Mus, Pris: 249.5 kr, Lager: 0 st
?>

Konstruktorer används för att initialisera objektets tillstånd direkt när det skapas, vilket ofta gör koden renare och säkrare.

Synlighet: public, private, protected

PHP tillåter oss att kontrollera hur egenskaper och metoder kan nås.

  • public: Kan nås och anropas varifrån som helst (utifrån objektet, inifrån klassen, från ärvande klasser).
  • private: Kan endast nås och anropas inifrån samma klass där den är definierad. Inte ens ärvande klasser kommer åt den.
  • protected: Kan nås och anropas inifrån samma klass och från klasser som ärver den, men inte direkt utifrån objektet.

Varför inte bara göra allt public? Att använda private (och protected) är en viktig del av encapsulation (inkapsling). Det hjälper till att:

  • Dölja implementationen: Hur klassen fungerar internt behöver inte exponeras utåt.
  • Kontrollera åtkomst: Du kan säkerställa att egenskaper bara ändras på ett kontrollerat sätt via metoder (getters/setters).
  • Förbättra underhåll: Om interna detaljer är private, kan du ändra dem utan att riskera att koden som använder klassen går sönder (så länge de publika metoderna fungerar likadant).

Exempel med private och Getters/Setters:

<?php
declare(strict_types=1);

class BankAccount
{
    private string $accountNumber;
    private float $balance;

    public function __construct(string $number, float $initialBalance = 0.0)
    {
        $this->accountNumber = $number;
        // Validera initialt saldo
        if ($initialBalance < 0) {
            $this->balance = 0.0;
        } else {
            $this->balance = $initialBalance;
        }
    }

    // Public Metod för att sätta in pengar (en "setter" för balansen, men med logik)
    public function deposit(float $amount): void
    {
        if ($amount > 0) {
            $this->balance += $amount;
        } else {
            echo "Insättningsbeloppet måste vara positivt.\n";
        }
    }

    // Public Metod för att ta ut pengar (en "setter" med logik)
    public function withdraw(float $amount): bool
    {
        if ($amount <= 0) {
            echo "Uttagsbeloppet måste vara positivt.\n";
            return false;
        }
        if ($amount > $this->balance) {
            echo "Inte tillräckligt med pengar på kontot.\n";
            return false;
        }
        $this->balance -= $amount;
        return true;
    }

    // Public Metod för att hämta saldot (en "getter")
    public function getBalance(): float
    {
        return $this->balance;
    }

    // Public Metod för att hämta kontonumret (en "getter")
    public function getAccountNumber(): string
    {
        return $this->accountNumber;
    }
}

$myAccount = new BankAccount('123-4567', 1000.0);

// echo $myAccount->balance; // Fatal error! Kan inte komma åt private property

echo "Saldo: " . $myAccount->getBalance() . " kr\n"; // Output: Saldo: 1000 kr

$myAccount->deposit(500.0);
echo "Nytt saldo: " . $myAccount->getBalance() . " kr\n"; // Output: Nytt saldo: 1500 kr

$myAccount->withdraw(2000.0); // Output: Inte tillräckligt med pengar på kontot.
echo "Saldo efter misslyckat uttag: " . $myAccount->getBalance() . " kr\n"; // Output: Saldo efter misslyckat uttag: 1500 kr

$success = $myAccount->withdraw(300.0);
if ($success) {
    echo "Saldo efter lyckat uttag: " . $myAccount->getBalance() . " kr\n"; // Output: Saldo efter lyckat uttag: 1200 kr
}
?>

I exemplet ovan är $balance och $accountNumber private. Vi kan inte läsa eller ändra dem direkt utifrån. Istället måste vi använda de public metoderna (deposit, withdraw, getBalance, getAccountNumber). Detta ger klassen full kontroll över hur saldot hanteras (t.ex. att man inte kan sätta in negativa belopp eller ta ut mer än vad som finns).

Fördelar med Klasser jämfört med Associativa Arrayer

  • Struktur och Tydlighet: En klass definierar en tydlig struktur för data.
  • Inkapsling: Kombinerar data (egenskaper) och beteende (metoder) som hör ihop.
  • Kontroll: Synlighetskontroller (private, protected) ger bättre kontroll över hur data nås och modifieras.
  • Återanvändbarhet: Klasser kan återanvändas för att skapa många objekt.
  • Underhåll: Ändringar i klassens interna implementation (om den är väl inkapslad) påverkar inte koden som använder klassen lika mycket.
  • Type Hinting: Möjliggör starkare typkontroll för både egenskaper och metodparametrar/returvärden.

Även om det kan verka som mer kod att skriva en klass initialt jämfört med en associativ array, lönar det sig snabbt i större och mer komplexa applikationer genom ökad struktur, säkerhet och underhållbarhet.

Detta är bara en introduktion till klasser. OOP i PHP inkluderar många fler koncept som arv, interfaces, traits, statiska metoder/egenskaper, m.m., men grunderna som täcks här är de mest väsentliga att börja med.

Introduktion till SQL och Relationsdatabaser

När vi bygger webbapplikationer behöver vi nästan alltid ett sätt att lagra, organisera och hämta data på ett beständigt sätt. Det kan vara användarinformation, produktlistor, blogginlägg eller vad som helst som applikationen behöver komma ihåg mellan besök och över tid. Här kommer databaser in i bilden.

En database (databas) är en organiserad samling av data. Det finns olika typer av databaser, men för många traditionella webbapplikationer är Relational Database Management Systems (Relationsdatabashanteringssystem, RDBMS) det vanligaste valet. Exempel på populära RDBMS inkluderar MySQL, MariaDB, PostgreSQL, SQLite och Microsoft SQL Server. I det här kapitlet fokuserar vi på MariaDB (som är mycket likt MySQL), eftersom det fungerar bra ihop med PHP.

Kärnan i en relationsdatabas är konceptet med tables (tabeller). En tabell organiserar data i:

  • Rows (Rader): Varje rad representerar en enskild post eller ett objekt (t.ex. en specifik användare, en produkt).
  • Columns (Kolumner): Varje kolumn representerar ett specifikt attribut eller egenskap för posterna (t.ex. användarens namn, produktens pris).
  • Relations (Relationer): Data i olika tabeller kan kopplas samman baserat på gemensamma värden, vilket gör att vi kan kombinera information (t.ex. koppla en order till den användare som lade den).

För att kommunicera med ett RDBMS – för att definiera tabellstrukturer, lägga till, ändra, ta bort och hämta data – använder vi ett standardiserat språk som heter SQL (Structured Query Language, Uttalas ofta "Sequel" eller S-Q-L).

Grundläggande SQL-Syntax

SQL består av olika statements (satser eller kommandon) som talar om för databasen vad den ska göra. Varje sats avslutas vanligtvis med ett semikolon (;), även om det ibland är valfritt beroende på verktyget man använder.

Viktiga nyckelord skrivs ofta med versaler av konvention (för läsbarhet), men SQL är generellt inte skiftlägeskänsligt för nyckelord. Tabell- och kolumnnamn kan dock vara skiftlägeskänsliga beroende på databasens konfiguration och operativsystemet.

Vanliga SQL-nyckelord inkluderar:

  • SELECT: Hämta data.
  • INSERT INTO: Lägga till nya rader.
  • UPDATE: Ändra befintliga rader.
  • DELETE: Ta bort rader.
  • CREATE TABLE: Skapa en ny tabell.
  • ALTER TABLE: Ändra en befintlig tabell.
  • DROP TABLE: Ta bort en tabell.
  • FROM: Anger vilken tabell data ska hämtas från.
  • WHERE: Filtrerar vilka rader som ska påverkas.
  • VALUES: Anger de värden som ska infogas.
  • SET: Anger vilka kolumner och värden som ska uppdateras.

Kommentarer i SQL skrivs antingen med två bindestreck (--) för en radskommentar, eller mellan /* och */ för flerradskommentarer.

-- Detta är en enradskommentar
SELECT name, email -- Hämta namn och e-post
FROM users
WHERE city = 'Stockholm'; /* Detta är en
   flerradskommentar */

Datatyper i SQL

När vi skapar en tabell måste vi specificera vilken typ av data varje kolumn ska innehålla. Detta kallas data type (datatyp). Att välja rätt datatyp är viktigt för:

  • Data Integrity (Dataintegritet): Säkerställer att endast giltig data lagras (t.ex. inga bokstäver i en sifferkolumn).
  • Storage Efficiency (Lagringseffektivitet): Olika datatyper tar olika mycket plats.
  • Performance (Prestanda): Korrekta datatyper kan snabba upp sökningar och jämförelser.

Här är några vanliga datatyper i SQL (specifika namn kan variera något mellan olika RDBMS, men koncepten är desamma):

  • Heltal:
    • INT eller INTEGER: Standard heltal (ofta 4 bytes).
    • TINYINT: Mycket litet heltal (ofta 1 byte, -128 till 127 eller 0 till 255).
    • BIGINT: Stort heltal (ofta 8 bytes).
  • Decimaltal:
    • DECIMAL(precision, scale): För exakta tal med fast antal decimaler (t.ex. DECIMAL(10, 2) för valuta). precision är totalt antal siffror, scale är antal decimaler.
    • FLOAT, DOUBLE: Flyttal för approximativa värden (undviks ofta för valuta).
  • Strängar (Text):
    • VARCHAR(n): Textsträng med variabel längd upp till n tecken.
    • TEXT: För längre textstycken utan specifik maxlängd (men det finns gränser).
    • CHAR(n): Textsträng med fast längd på n tecken (fylls ut med mellanslag om kortare).
  • Datum och Tid:
    • DATE: Endast datum (YYYY-MM-DD).
    • TIME: Endast tid (HH:MM:SS).
    • DATETIME: Både datum och tid (YYYY-MM-DD HH:MM:SS).
    • TIMESTAMP: Liknar DATETIME men används ofta för att automatiskt spåra när en rad skapades eller senast ändrades.
  • Boolean (Logiskt värde): SQL har ingen standard BOOLEAN-typ. Istället används ofta TINYINT(1) där 0 representerar false och 1 representerar true.

En kolumn kan också tillåta NULL-värden, vilket betyder att värdet är okänt eller saknas. Motsatsen är att definiera kolumnen med NOT NULL, vilket kräver att ett värde alltid anges.

Skapa och Hantera Tabeller (DDL - Data Definition Language)

SQL-satser som används för att definiera eller ändra strukturen på databasobjekt (som tabeller) kallas DDL (Data Definition Language).

CREATE TABLE

Används för att skapa en ny tabell.

CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY, -- Unik identifierare för varje användare
    name VARCHAR(100) NOT NULL,       -- Användarens namn (max 100 tecken, får ej vara tomt)
    email VARCHAR(150) NOT NULL UNIQUE, -- E-post (max 150 tecken, får ej vara tomt, måste vara unikt)
    city VARCHAR(50),                   -- Stad (max 50 tecken, får vara tomt - NULL tillåtet)
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -- Tidsstämpel när raden skapades
);

Förklaringar:

  • CREATE TABLE users (...): Startar definitionen av en tabell med namnet users.
  • id INT AUTO_INCREMENT PRIMARY KEY: Definierar en kolumn id av typen INT. AUTO_INCREMENT gör att databasen automatiskt tilldelar ett nytt, unikt nummer för varje ny rad. PRIMARY KEY (Primärnyckel) markerar denna kolumn som den unika identifieraren för varje rad i tabellen. Ingen rad får ha samma id, och id får inte vara NULL.
  • name VARCHAR(100) NOT NULL: En textkolumn för namn, max 100 tecken. NOT NULL betyder att denna kolumn måste ha ett värde.
  • email VARCHAR(150) NOT NULL UNIQUE: En textkolumn för e-post. UNIQUE säkerställer att ingen annan rad i tabellen kan ha samma e-postadress.
  • city VARCHAR(50): En textkolumn för stad. Eftersom NOT NULL saknas, tillåts NULL-värden här.
  • created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP: En tidsstämpelkolumn. DEFAULT CURRENT_TIMESTAMP betyder att om inget värde anges vid INSERT, sätts kolumnen automatiskt till den aktuella tidpunkten.

ALTER TABLE

Används för att modifiera en befintlig tabell.

-- Lägg till en ny kolumn 'phone_number'
ALTER TABLE users
ADD COLUMN phone_number VARCHAR(20);

-- Ändra datatypen för 'city'-kolumnen
ALTER TABLE users
MODIFY COLUMN city VARCHAR(60);

-- Ta bort 'phone_number'-kolumnen
ALTER TABLE users
DROP COLUMN phone_number;

DROP TABLE

Används för att permanent ta bort en tabell och all dess data. Använd med extrem försiktighet!

DROP TABLE users;

Not: I moderna projekt används ofta migrationsverktyg (som Phinx, Doctrine Migrations, eller inbyggda i ramverk som Laravel) för att hantera databasstrukturförändringar på ett kontrollerat och versionshanterat sätt, istället för att köra ALTER TABLE manuellt.

Hämta Data (SELECT)

Den absolut vanligaste SQL-satsen är SELECT, som används för att hämta data från en eller flera tabeller. Detta är en del av DML (Data Manipulation Language).

Grundläggande SELECT

-- Hämta specifika kolumner från tabellen 'users'
SELECT name, email
FROM users;

-- Hämta alla kolumner (* är en wildcard för alla)
SELECT *
FROM users;

Filtrera Rader (WHERE)

WHERE-klausulen används för att specificera villkor som raderna måste uppfylla för att inkluderas i resultatet.

-- Hämta användare från Stockholm
SELECT name, email
FROM users
WHERE city = 'Stockholm';

-- Hämta användare som INTE är från Stockholm
SELECT name, email
FROM users
WHERE city != 'Stockholm'; -- Eller city <> 'Stockholm'

-- Hämta användare vars id är större än 10
SELECT id, name
FROM users
WHERE id > 10;

-- Hämta användare från Stockholm ELLER Göteborg
SELECT name, city
FROM users
WHERE city = 'Stockholm' OR city = 'Göteborg';

-- Alternativt med IN
SELECT name, city
FROM users
WHERE city IN ('Stockholm', 'Göteborg');

-- Hämta användare vars namn börjar på 'A'
-- % är en wildcard som matchar noll eller flera tecken
SELECT name
FROM users
WHERE name LIKE 'A%';

-- Hämta användare vars namn slutar på 'son'
SELECT name
FROM users
WHERE name LIKE '%son';

-- Hämta användare vars namn innehåller 'an'
SELECT name
FROM users
WHERE name LIKE '%an%';

-- Hämta användare där staden inte är angiven (är NULL)
SELECT name
FROM users
WHERE city IS NULL;

-- Hämta användare där staden ÄR angiven (inte NULL)
SELECT name, city
FROM users
WHERE city IS NOT NULL;

-- Hämta användare från Stockholm som heter Anna
SELECT name, email, city
FROM users
WHERE city = 'Stockholm' AND name = 'Anna';

Sortera Resultat (ORDER BY)

ORDER BY används för att sortera resultatet baserat på en eller flera kolumner.

-- Hämta alla användare sorterade efter namn i bokstavsordning (stigande, ASC är default)
SELECT name, email
FROM users
ORDER BY name ASC; -- ASC kan utelämnas

-- Hämta alla användare sorterade efter id i fallande ordning (högst id först)
SELECT id, name
FROM users
ORDER BY id DESC;

-- Sortera först på stad, sedan på namn inom varje stad
SELECT name, city
FROM users
ORDER BY city, name;

Begränsa Antal Rader (LIMIT)

LIMIT används för att begränsa antalet rader som returneras. Används ofta för paginering (att visa data sida för sida).

-- Hämta de 5 första användarna (baserat på default ordning eller ORDER BY)
SELECT id, name
FROM users
ORDER BY id
LIMIT 5;

-- Hämta 10 användare, men hoppa över de första 20 (används för paginering, sida 3 om 10 per sida)
-- LIMIT offset, count
SELECT id, name
FROM users
ORDER BY id
LIMIT 20, 10;

Lägga till Data (INSERT INTO)

INSERT INTO används för att lägga till nya rader i en tabell.

-- Lägg till en ny användare, specificera kolumner
INSERT INTO users (name, email, city)
VALUES ('Kalle Anka', 'kalle@example.com', 'Ankeborg');

-- Lägg till en ny användare utan stad (om 'city' tillåter NULL)
INSERT INTO users (name, email)
VALUES ('Musse Pigg', 'musse@example.com');

-- Lägg till flera användare samtidigt (syntax kan variera lite)
INSERT INTO users (name, email, city)
VALUES
    ('Långben', 'langben@example.com', 'Ankeborg'),
    ('Joakim von Anka', 'joakim@example.com', NULL);

Not: Om du har en AUTO_INCREMENT-kolumn (som id i vårt exempel) behöver du inte (och ska oftast inte) ange den i INSERT-satsen. Databasen sköter det automatiskt.

Uppdatera Data (UPDATE)

UPDATE används för att ändra data i befintliga rader.

-- Uppdatera staden för användaren med id 5
UPDATE users
SET city = 'Göteborg'
WHERE id = 5;

-- Uppdatera både namn och e-post för användaren med id 10
UPDATE users
SET name = 'Kajsa Anka', email = 'kajsa.anka@example.com'
WHERE id = 10;

-- Uppdatera ALLA användare (MYCKET FARLIGT UTAN WHERE!)
-- UPDATE users SET city = 'Okänd stad'; -- Gör INTE detta utan att vara säker!

VARNING: WHERE-klausulen i UPDATE är extremt viktig. Utan den kommer alla rader i tabellen att uppdateras!

Ta bort Data (DELETE)

DELETE används för att ta bort rader från en tabell.

-- Ta bort användaren med id 15
DELETE FROM users
WHERE id = 15;

-- Ta bort alla användare från Ankeborg
DELETE FROM users
WHERE city = 'Ankeborg';

-- Ta bort ALLA användare (MYCKET FARLIGT UTAN WHERE!)
-- DELETE FROM users; -- Gör INTE detta utan att vara säker!

VARNING: Precis som med UPDATE, är WHERE-klausulen i DELETE kritisk. Utan den raderas alla rader i tabellen!

Sammanfogning av Tabeller (JOIN)

Ofta är den data vi behöver spridd över flera relaterade tabeller. Till exempel kanske vi har en users-tabell och en orders-tabell, där orders-tabellen innehåller en referens till användaren som lade ordern.

För att kombinera data från flera tabeller i en enda fråga använder vi JOIN.

Låt oss anta att vi har tabellerna:

users

idname
1Alice
2Bob
3Charlie

orders

order_iduser_idproduct
1011Laptop
1022Keyboard
1031Mouse
1044Monitor

Observera att orders.user_id refererar till users.id. Användare med id 3 har inga ordrar, och order 104 tillhör en användare (4) som inte finns i users-tabellen (i detta exempel).

INNER JOIN

Den vanligaste typen är INNER JOIN. Den returnerar endast rader där det finns en matchning i båda tabellerna baserat på JOIN-villkoret.

SELECT
    users.name,      -- Hämta användarens namn
    orders.product   -- Hämta produktnamnet från ordern
FROM
    users            -- Börja med users-tabellen
INNER JOIN
    orders           -- Koppla ihop med orders-tabellen
ON
    users.id = orders.user_id; -- Villkoret för kopplingen

Resultat:

nameproduct
AliceLaptop
BobKeyboard
AliceMouse
  • Alice (id 1) finns i båda tabellerna via user_id 1 i orders (två gånger).
  • Bob (id 2) finns i båda tabellerna via user_id 2 i orders.
  • Charlie (id 3) finns bara i users, så han inkluderas inte.
  • Order 104 (user_id 4) har ingen matchande användare i users, så den inkluderas inte.

Visualisering av INNER JOIN:

graph LR
    subgraph Users Table
        U1["id: 1<br>name: 'Alice'"]
        U2["id: 2<br>name: 'Bob'"]
        U3["id: 3<br>name: 'Charlie'"]
    end
    subgraph Orders Table
        O1["id: 101<br>user_id: 1<br>product: 'Laptop'"]
        O2["id: 102<br>user_id: 2<br>product: 'Keyboard'"]
        O3["id: 103<br>user_id: 1<br>product: 'Mouse'"]
        O4["id: 104<br>user_id: 4<br>product: 'Monitor'"]
    end
    subgraph INNER JOIN Result (on users.id = orders.user_id)
        R1["user.name: 'Alice'<br>order.product: 'Laptop'"]
        R2["user.name: 'Bob'<br>order.product: 'Keyboard'"]
        R3["user.name: 'Alice'<br>order.product: 'Mouse'"]
    end

    U1 -- "match" --> R1
    U1 -- "match" --> R3
    U2 -- "match" --> R2
    O1 -- "match" --> R1
    O2 -- "match" --> R2
    O3 -- "match" --> R3

    style U3 fill:#f9f,stroke:#333,stroke-width:2px
    style O4 fill:#f9f,stroke:#333,stroke-width:2px

(De rosa noderna representerar rader som inte inkluderas i INNER JOIN-resultatet.)

Andra JOIN-typer (Kort)

  • LEFT JOIN: Returnerar alla rader från den vänstra tabellen (users i exemplet ovan) och matchande rader från den högra (orders). Om ingen matchning finns i den högra tabellen, fylls dess kolumner ut med NULL. (Charlie skulle komma med, men med NULL i produktkolumnen).
  • RIGHT JOIN: Motsatsen till LEFT JOIN. Returnerar alla rader från den högra tabellen och matchande från vänstra. (Order 104 skulle komma med, men med NULL i namnkolumnen).
  • FULL OUTER JOIN: Returnerar alla rader från båda tabellerna. Om matchning saknas fylls kolumnerna från den andra tabellen ut med NULL. (Stöds inte direkt av MySQL/MariaDB, men kan simuleras).

SQL och PHP (Förhandstitt)

Nu när du har en grundläggande förståelse för SQL-satser är nästa steg att se hur vi kan exekvera dessa från vår PHP-kod. PHP erbjuder olika sätt att ansluta till och interagera med databaser som MariaDB/MySQL:

  1. PDO (PHP Data Objects): Ett databasabstraktionslager som ger ett konsekvent gränssnitt för att arbeta med olika databastyper.
  2. MySQLi (MySQL Improved Extension): En specifik extension för att arbeta med MySQL och MariaDB.

Vi kommer jobba främst med PDO.

I de kommande avsnittet crud-app.md kommer vi att dyka djupare in i hur man använder PDO för att:

  • Ansluta till databasen från PHP.
  • Skicka SQL-frågor (SELECT, INSERT, UPDATE, DELETE) till databasen.
  • Hantera resultaten från SELECT-frågor.
  • Skydda sig mot SQL Injection (SQL-injektion), en allvarlig säkerhetsrisk, med hjälp av Prepared Statements (förberedda uttryck).

Att kunna SQL är en grundläggande färdighet för fullstack-utveckling, eftersom det låter dig interagera med den data som driver din applikation.

Bygga en CRUD-applikation: Enkel Blogg

I detta avsnitt bygger vi en komplett men enkel bloggapplikation från grunden med PHP och MariaDB/MySQL. Målet är att praktiskt demonstrera CRUD-operationerna (Create, Read, Update, Delete) och integrera andra viktiga webbkoncept som databasinteraktion med PDO, användarautentisering, sessionshantering och filuppladdning.

Vi börjar med en mycket grundläggande struktur och kodstil för att sedan i slutet refaktorera och förbättra koden, bland annat genom att introducera type hinting.

Applikationens Funktioner:

  • Användare kan registrera sig och logga in.
  • Inloggade användare kan skapa, redigera och ta bort sina egna blogginlägg.
  • Användare kan ladda upp en bild till varje blogginlägg.
  • Alla besökare kan se listan över blogginlägg och läsa enskilda inlägg.
  • En enkel "admin"-sektion för inloggade användare att hantera sina inlägg.

1. Databasdesign

Vi behöver två huvudtabeller: en för användare (users) och en för blogginlägg (posts).

users tabell:

CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) NOT NULL UNIQUE,
    email VARCHAR(100) NOT NULL UNIQUE,
    password_hash VARCHAR(255) NOT NULL, -- Lagrar hashat lösenord
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
  • id: Unik identifierare för användaren.
  • username: Användarnamn för inloggning (unikt).
  • email: Användarens e-post (unik).
  • password_hash: Lagrar det säkert hashade lösenordet (inte lösenordet i klartext!).
  • created_at: När användarkontot skapades.

posts tabell:

CREATE TABLE posts (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT NOT NULL,             -- Vem som skrev inlägget
    title VARCHAR(255) NOT NULL,      -- Inläggets titel
    body TEXT NOT NULL,               -- Innehållet i inlägget
    image_path VARCHAR(255) NULL,     -- Sökväg till uppladdad bild (valfritt)
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, -- När inlägget senast ändrades
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE -- Koppling till users. Om användaren raderas, raderas även hens inlägg.
);
  • id: Unik identifierare för inlägget.
  • user_id: Referens till id i users-tabellen. Visar vem som äger inlägget.
  • title: Inläggets rubrik.
  • body: Brödtexten i inlägget.
  • image_path: Sökvägen till den associerade bilden (om någon laddats upp). NULL om ingen bild finns.
  • created_at: När inlägget skapades.
  • updated_at: Uppdateras automatiskt när inlägget ändras.
  • FOREIGN KEY ...: Definierar en relation mellan posts.user_id och users.id. ON DELETE CASCADE betyder att om en användare tas bort, tas alla dennes inlägg också bort automatiskt.

Skapa dessa tabeller i din MariaDB/MySQL-databas (t.ex. via phpMyAdmin eller kommandoraden). Vi antar att databasen heter db_fullstack som i tidigare exempel.

2. Projektstruktur (Initial)

Vi börjar med en väldigt platt och enkel filstruktur. Skapa följande filer och mappar i roten av ditt projekt (t.ex. i app/public om du följer docker-compose-exemplet):

.
├── admin/
│   ├── index.php         # Admin dashboard (lista inlägg, länkar till create/edit/delete)
│   ├── create_post.php   # Formulär & logik för att skapa inlägg
│   ├── edit_post.php     # Formulär & logik för att redigera inlägg
│   └── delete_post.php   # Logik för att ta bort inlägg
├── includes/
│   ├── config.php        # Databasuppgifter och annan konfiguration
│   ├── database.php      # Funktion för att ansluta till databasen (PDO)
│   └── functions.php     # Hjälpfunktioner (vi lägger till här senare)
├── uploads/              # Mapp där uppladdade bilder sparas (måste vara skrivbar för webbservern!)
├── index.php             # Hemsida, listar blogginlägg
├── login.php             # Inloggningsformulär & logik
├── logout.php            # Logik för att logga ut
├── post.php              # Visar ett enskilt blogginlägg
└── register.php          # Registreringsformulär & logik
  • admin/: Innehåller sidor som endast inloggade användare ska kunna nå.
  • includes/: Innehåller återanvändbar kod som konfiguration och databasanslutning.
  • uploads/: Här hamnar bilder som användare laddar upp. VIKTIGT: Se till att webbservern (Apache i vårt Docker-exempel) har skrivrättigheter till denna mapp!

3. Grundläggande Setup

Konfiguration (includes/config.php)

Skapa filen includes/config.php och lägg in dina databasuppgifter.

<?php
// includes/config.php

// Databasuppgifter (anpassa efter din miljö)
define('DB_HOST', 'mysql'); // Matchar service-namnet i docker-compose.yml
define('DB_NAME', 'db_fullstack');
define('DB_USER', 'db_user');
define('DB_PASS', 'db_password');

// Teckenkodning för PDO-anslutningen
define('DB_CHARSET', 'utf8mb4');

// Starta sessioner (viktigt för login!)
// Görs en gång här så det gäller alla sidor som inkluderar config.php
if (session_status() === PHP_SESSION_NONE) {
    session_start();
}

// Base URL (valfritt, men kan vara praktiskt för länkar)
// Anpassa port om du använder en annan än 8060
define('BASE_URL', 'http://localhost:8060');

// Sökväg till uppladdningsmappen
define('UPLOAD_PATH', __DIR__ . '/../uploads/'); // __DIR__ ger sökvägen till includes/

// Aktivera felrapportering under utveckling
// Stäng av på en produktionsserver!
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);

?>

Databasanslutning (includes/database.php)

Skapa filen includes/database.php. Vi använder PDO (PHP Data Objects) för att ansluta. PDO ger ett konsekvent sätt att interagera med olika databaser och hjälper till att skydda mot SQL-injektion genom "prepared statements".

<?php
// includes/database.php
require_once 'config.php'; // Inkludera konfigurationen

/**
 * Skapar och returnerar en PDO-databasanslutning.
 *
 * @return PDO PDO-anslutningsobjektet.
 * @throws PDOException Om anslutningen misslyckas.
 */
function connect_db(): PDO {
    // Data Source Name (DSN)
    $dsn = "mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=" . DB_CHARSET;

    // Alternativ för PDO-anslutningen
    $options = [
        PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION, // Kasta exceptions vid fel
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,       // Hämta resultat som associativa arrayer
        PDO::ATTR_EMULATE_PREPARES   => false,                  // Använd prepared statements
    ];

    try {
        $pdo = new PDO($dsn, DB_USER, DB_PASS, $options);
        return $pdo;
    } catch (PDOException $e) {
        // Logga felet istället för att skriva ut känslig info
        error_log("Database Connection Error: " . $e->getMessage());
        // Visa ett generellt felmeddelande till användaren
        throw new PDOException("Kunde inte ansluta till databasen. Försök igen senare.", (int)$e->getCode());
        // Eller: die("Kunde inte ansluta till databasen. Kontakta administratör.");
    }
}

// För att använda anslutningen på en sida:
// $pdo = connect_db();

?>

Förklaring:

  • Vi inkluderar config.php för att få databasuppgifterna.
  • $dsn: En sträng som specificerar databastyp, värd, databasnamn och teckenkodning.
  • $options: Viktiga inställningar för PDO:
    • PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION: Gör att PDO kastar ett undantag (PDOException) om ett databasfel inträffar. Detta är att föredra framför att bara få false eller varningar, då det ger tydligare felhantering.
    • PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC: Gör att när vi hämtar data (fetch(), fetchAll()) får vi resultatet som en associativ array (kolumnnamn => värde) istället för både numeriska index och kolumnnamn (vilket är default PDO::FETCH_BOTH).
    • PDO::ATTR_EMULATE_PREPARES => false: Tvingar PDO att använda databasserverns inbyggda "prepared statements" istället för att emulera dem i PHP. Detta är generellt säkrare och ibland mer effektivt.
  • try...catch: Vi försöker skapa ett nytt PDO-objekt. Om det misslyckas (fel lösenord, databasen nere etc.) kastas en PDOException. Vi fångar den, loggar det tekniska felet (för utvecklaren) och kastar sedan ett nytt, mer användarvänligt, undantag eller avslutar skriptet med die(). Skriv aldrig ut det detaljerade felet $e->getMessage() direkt till användaren i produktion!

4. Användarregistrering (register.php)

Nu skapar vi sidan där användare kan registrera sig.

register.php:

<?php
require_once 'includes/config.php';
require_once 'includes/database.php';

$errors = []; // Array för att lagra felmeddelanden
$username = '';
$email = '';

// Hantera formulärdata när det skickas (POST request)
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $username = trim($_POST['username'] ?? '');
    $email = trim($_POST['email'] ?? '');
    $password = $_POST['password'] ?? '';
    $confirm_password = $_POST['confirm_password'] ?? '';

    // Validering
    if (empty($username)) {
        $errors[] = 'Användarnamn är obligatoriskt.';
    }
    if (empty($email)) {
        $errors[] = 'E-post är obligatoriskt.';
    } elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
        $errors[] = 'Ogiltig e-postadress.';
    }
    if (empty($password)) {
        $errors[] = 'Lösenord är obligatoriskt.';
    } elseif (strlen($password) < 6) {
        $errors[] = 'Lösenordet måste vara minst 6 tecken långt.';
    }
    if ($password !== $confirm_password) {
        $errors[] = 'Lösenorden matchar inte.';
    }

    // Om inga valideringsfel, försök registrera användaren
    if (empty($errors)) {
        try {
            $pdo = connect_db();

            // 1. Kolla om användarnamn eller e-post redan finns
            $stmt_check = $pdo->prepare("SELECT id FROM users WHERE username = :username OR email = :email");
            $stmt_check->bindParam(':username', $username);
            $stmt_check->bindParam(':email', $email);
            $stmt_check->execute();

            if ($stmt_check->fetch()) {
                $errors[] = 'Användarnamn eller e-postadress är redan registrerad.';
            } else {
                // 2. Hasha lösenordet säkert
                $password_hash = password_hash($password, PASSWORD_DEFAULT);

                // 3. Infoga användaren i databasen
                $stmt_insert = $pdo->prepare("INSERT INTO users (username, email, password_hash) VALUES (:username, :email, :password_hash)");
                $stmt_insert->bindParam(':username', $username);
                $stmt_insert->bindParam(':email', $email);
                $stmt_insert->bindParam(':password_hash', $password_hash);

                if ($stmt_insert->execute()) {
                    // Registrering lyckades! Omdirigera till login-sidan.
                    header('Location: login.php?registered=success');
                    exit;
                } else {
                    $errors[] = 'Ett fel uppstod vid registrering. Försök igen.';
                }
            }
        } catch (PDOException $e) {
            error_log("Registration Error: " . $e->getMessage());
            $errors[] = 'Databasfel. Kan inte registrera användare just nu.';
        }
    }
}
?>
<!DOCTYPE html>
<html lang="sv">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Registrera dig - Enkel Blogg</title>
    <style> /* Enkel CSS för formuläret */
        body { font-family: sans-serif; line-height: 1.6; padding: 20px; }
        .form-group { margin-bottom: 15px; }
        label { display: block; margin-bottom: 5px; }
        input[type="text"], input[type="email"], input[type="password"] {
            width: 100%; padding: 8px; border: 1px solid #ccc; box-sizing: border-box;
        }
        button { padding: 10px 15px; background-color: #007bff; color: white; border: none; cursor: pointer; }
        button:hover { background-color: #0056b3; }
        .error-messages { color: red; margin-bottom: 15px; }
        .error-messages ul { list-style: none; padding: 0; }
    </style>
</head>
<body>
    <h1>Registrera nytt konto</h1>

    <?php if (!empty($errors)): ?>
        <div class="error-messages">
            <strong>Registreringen misslyckades:</strong>
            <ul>
                <?php foreach ($errors as $error): ?>
                    <li><?php echo htmlspecialchars($error); ?></li>
                <?php endforeach; ?>
            </ul>
        </div>
    <?php endif; ?>

    <form action="register.php" method="post">
        <div class="form-group">
            <label for="username">Användarnamn:</label>
            <input type="text" id="username" name="username" value="<?php echo htmlspecialchars($username); ?>" required>
        </div>
        <div class="form-group">
            <label for="email">E-post:</label>
            <input type="email" id="email" name="email" value="<?php echo htmlspecialchars($email); ?>" required>
        </div>
        <div class="form-group">
            <label for="password">Lösenord:</label>
            <input type="password" id="password" name="password" required minlength="6">
        </div>
        <div class="form-group">
            <label for="confirm_password">Bekräfta lösenord:</label>
            <input type="password" id="confirm_password" name="confirm_password" required>
        </div>
        <button type="submit">Registrera</button>
    </form>

    <p>Har du redan ett konto? <a href="login.php">Logga in här</a>.</p>
</body>
</html>

Förklaring av register.php:

  1. Inkludera filer: Börjar med att inkludera config.php (som startar sessioner) och database.php.
  2. Felhantering: Initierar en $errors-array för att samla validerings- och andra fel.
  3. POST-hantering: Kollar om formuläret skickats ($_SERVER['REQUEST_METHOD'] === 'POST').
  4. Hämta data: Tar emot data från $_POST. trim() tar bort onödiga mellanslag. ?? '' ger en tom sträng om värdet saknas.
  5. Validering: Kontrollerar att fälten inte är tomma, att e-post är giltig (filter_var), att lösenordet är tillräckligt långt och att lösenorden matchar.
  6. Databaskontroll:
    • Om validering lyckas, anslut till databasen inom en try...catch-block.
    • Prepared Statement (Check): Skapar en förberedd fråga (prepare) för att kolla om användarnamn eller e-post redan finns. bindParam binder PHP-variabler till platshållare (:username, :email). Detta är avgörande för säkerhet mot SQL-injektion.
    • execute() kör frågan.
    • fetch() hämtar en rad. Om den returnerar något, finns användaren redan.
  7. Lösenordshashning:
    • VIKTIGT: Lagra aldrig lösenord i klartext! Använd password_hash() med PASSWORD_DEFAULT. PHP väljer då den starkaste tillgängliga hash-algoritmen (oftast bcrypt). Varje gång detta körs genereras en unik hash, även för samma lösenord.
  8. Infoga Användare:
    • Prepared Statement (Insert): Skapar en ny förberedd fråga för att infoga användaren.
    • Binder parametrar igen.
    • execute() kör infogningen.
  9. Omdirigering: Om infogningen lyckas (execute() returnerar true), omdirigera användaren till login.php med header('Location: ...'). exit; efteråt är viktigt för att stoppa skriptet.
  10. Felvisning: Om $errors-arrayen inte är tom (antingen från validering eller databasfel), skrivs felmeddelandena ut ovanför formuläret. htmlspecialchars() används för att förhindra Cross-Site Scripting (XSS) när användarinmatning (som $username, $email) skrivs ut.

5. Användarlogin (login.php)

Nu skapar vi inloggningssidan.

login.php:

<?php
require_once 'includes/config.php'; // Startar sessionen
require_once 'includes/database.php';

$errors = [];
$username = '';

// Visa meddelande om registrering lyckades
$registration_success = isset($_GET['registered']) && $_GET['registered'] === 'success';

// Hantera formulärdata när det skickas (POST request)
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $username = trim($_POST['username'] ?? '');
    $password = $_POST['password'] ?? '';

    // Validering
    if (empty($username)) {
        $errors[] = 'Användarnamn är obligatoriskt.';
    }
    if (empty($password)) {
        $errors[] = 'Lösenord är obligatoriskt.';
    }

    // Om inga valideringsfel, försök logga in
    if (empty($errors)) {
        try {
            $pdo = connect_db();

            // 1. Hämta användaren från databasen baserat på användarnamn
            $stmt = $pdo->prepare("SELECT id, username, password_hash FROM users WHERE username = :username");
            $stmt->bindParam(':username', $username);
            $stmt->execute();
            $user = $stmt->fetch(); // Hämta användardata (eller false om användaren inte finns)

            // 2. Verifiera lösenordet
            if ($user && password_verify($password, $user['password_hash'])) {
                // Lösenordet matchar! Logga in användaren.

                // Regenerera session ID för säkerhet (mot session fixation)
                session_regenerate_id(true);

                // Spara användarinformation i sessionen
                $_SESSION['user_id'] = $user['id'];
                $_SESSION['username'] = $user['username'];

                // Omdirigera till admin-sidan eller startsidan
                header('Location: admin/index.php');
                exit;

            } else {
                // Användare finns inte eller fel lösenord
                $errors[] = 'Felaktigt användarnamn eller lösenord.';
            }

        } catch (PDOException $e) {
            error_log("Login Error: " . $e->getMessage());
            $errors[] = 'Databasfel. Kan inte logga in just nu.';
        }
    }
}
?>
<!DOCTYPE html>
<html lang="sv">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Logga in - Enkel Blogg</title>
    <style> /* Samma enkla CSS som register.php */
        body { font-family: sans-serif; line-height: 1.6; padding: 20px; }
        .form-group { margin-bottom: 15px; }
        label { display: block; margin-bottom: 5px; }
        input[type="text"], input[type="password"] {
            width: 100%; padding: 8px; border: 1px solid #ccc; box-sizing: border-box;
        }
        button { padding: 10px 15px; background-color: #007bff; color: white; border: none; cursor: pointer; }
        button:hover { background-color: #0056b3; }
        .error-messages { color: red; margin-bottom: 15px; }
        .error-messages ul { list-style: none; padding: 0; }
        .success-message { color: green; margin-bottom: 15px; }
    </style>
</head>
<body>
    <h1>Logga in</h1>

    <?php if ($registration_success): ?>
        <p class="success-message">Registreringen lyckades! Du kan nu logga in.</p>
    <?php endif; ?>

    <?php if (!empty($errors)): ?>
        <div class="error-messages">
            <strong>Inloggningen misslyckades:</strong>
            <ul>
                <?php foreach ($errors as $error): ?>
                    <li><?php echo htmlspecialchars($error); ?></li>
                <?php endforeach; ?>
            </ul>
        </div>
    <?php endif; ?>

    <form action="login.php" method="post">
        <div class="form-group">
            <label for="username">Användarnamn:</label>
            <input type="text" id="username" name="username" value="<?php echo htmlspecialchars($username); ?>" required>
        </div>
        <div class="form-group">
            <label for="password">Lösenord:</label>
            <input type="password" id="password" name="password" required>
        </div>
        <button type="submit">Logga in</button>
    </form>

    <p>Har du inget konto? <a href="register.php">Registrera dig här</a>.</p>
</body>
</html>

Förklaring av login.php:

  1. Session Start: config.php startar sessionen (session_start()).
  2. Framgångsmeddelande: Kollar om ?registered=success finns i URL:en och visar i så fall ett meddelande.
  3. POST-hantering & Validering: Liknar register.php.
  4. Hämta Användare:
    • Använder en prepared statement för att hämta användaren baserat på det angivna användarnamnet. Vi hämtar id, username och den hashade lösenordet (password_hash).
    • $stmt->fetch() returnerar antingen en associativ array med användardata eller false om användarnamnet inte hittades.
  5. Verifiera Lösenord:
    • VIKTIGT: Använd password_verify($password, $user['password_hash']) för att jämföra det angivna lösenordet ($password) med det hashade lösenordet från databasen ($user['password_hash']). password_verify sköter allt (salt etc.) automatiskt. Returnerar true om lösenordet matchar, annars false. Jämför aldrig hashar direkt!
  6. Logga in (Session):
    • Om $user finns OCH password_verify returnerar true, är inloggningen lyckad.
    • session_regenerate_id(true): Skapar ett nytt session-ID för den aktuella sessionen. Detta är en viktig säkerhetsåtgärd för att förhindra "session fixation"-attacker. Det gamla session-ID:t tas bort.
    • $_SESSION['user_id'] = $user['id'];: Vi sparar användarens id i $_SESSION-arrayen. Detta är nyckeln till att veta att användaren är inloggad på andra sidor.
    • $_SESSION['username'] = $user['username'];: Sparar även användarnamnet för att kunna visa det.
  7. Omdirigering: Omdirigera den inloggade användaren till admin/index.php.
  8. Felhantering: Om $user inte finns eller password_verify misslyckas, visa ett generellt felmeddelande.
  9. HTML-formulär: Liknar registreringsformuläret.

6. Sessionshantering & Logout

Hur Sessioner Fungerar (Kort)

  1. session_start(): Startar eller återupptar en session. PHP letar efter ett session-ID (oftast skickat via en cookie). Om inget finns, skapas ett nytt ID och en ny sessionfil på servern. Om ett ID finns, laddas data från motsvarande sessionfil in i den globala $_SESSION-arrayen.
  2. $_SESSION: En superglobal array där vi kan lagra data som är specifik för den aktuella användarens session (t.ex. user_id). Datan sparas på servern.
  3. Session Cookie: PHP skickar automatiskt en cookie (oftast med namnet PHPSESSID) till webbläsaren som innehåller det unika session-ID:t. Webbläsaren skickar tillbaka denna cookie vid varje efterföljande förfrågan, så att PHP kan identifiera rätt sessionfil.

Skydda Sidor

För sidor som bara inloggade användare ska nå (t.ex. i admin/-mappen), behöver vi kontrollera om $_SESSION['user_id'] är satt.

Lägg till följande kod i början av varje skyddad fil (t.ex. admin/index.php, admin/create_post.php etc.):

<?php
require_once '../includes/config.php'; // Gå upp en nivå för att nå includes

// Kontrollera om användaren är inloggad
if (!isset($_SESSION['user_id'])) {
    // Användaren är inte inloggad, omdirigera till login-sidan
    header('Location: ../login.php?redirect=' . urlencode($_SERVER['REQUEST_URI']));
    exit;
}

// Om vi når hit är användaren inloggad.
// Hämta användar-ID från sessionen för senare användning
$logged_in_user_id = $_SESSION['user_id'];
$logged_in_username = $_SESSION['username']; // Kan användas för att visa "Inloggad som ..."

// Inkludera resten av sidans logik här...
require_once '../includes/database.php';
// ...
?>
  • Vi kollar om $_SESSION['user_id'] är satt. Om inte, omdirigerar vi till login.php.
  • Vi skickar med ?redirect=... för att potentiellt kunna skicka tillbaka användaren till den sida de försökte nå efter inloggning (implementeras inte fullt ut här, men är en bra idé). urlencode ser till att URL:en är korrekt kodad.

Utloggning (logout.php)

En enkel sida för att avsluta sessionen.

logout.php:

<?php
require_once 'includes/config.php'; // Säkerställer att sessionen startas

// 1. Ta bort alla sessionsvariabler
$_SESSION = []; // Tömmer $_SESSION-arrayen

// 2. Om sessionskakor används, ta bort den
if (ini_get("session.use_cookies")) {
    $params = session_get_cookie_params();
    setcookie(session_name(), '', time() - 42000, // Sätt en tidpunkt i det förflutna
        $params["path"], $params["domain"],
        $params["secure"], $params["httponly"]
    );
}

// 3. Förstör sessionen på servern
session_destroy();

// 4. Omdirigera till startsidan eller login-sidan
header('Location: index.php?logged_out=success');
exit;
?>

Förklaring av logout.php:

  1. Tömmer $_SESSION-arrayen.
  2. Tar bort sessionscookien från webbläsaren genom att sätta dess utgångstid i det förflutna.
  3. session_destroy(): Tar bort sessionfilen på servern.
  4. Omdirigerar användaren.

7. Skapa Blogginlägg (Create - admin/create_post.php)

Denna sida innehåller formuläret och logiken för att skapa ett nytt blogginlägg. Den måste vara skyddad.

admin/create_post.php:

<?php
require_once '../includes/config.php'; // Ger session_start()

// ---- Session Check ----
if (!isset($_SESSION['user_id'])) {
    header('Location: ../login.php?redirect=' . urlencode($_SERVER['REQUEST_URI']));
    exit;
}
$logged_in_user_id = $_SESSION['user_id'];
// ---- End Session Check ----

require_once '../includes/database.php';

$errors = [];
$title = '';
$body = '';

// Hantera formulärdata när det skickas (POST request)
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $title = trim($_POST['title'] ?? '');
    $body = trim($_POST['body'] ?? '');
    $image = $_FILES['image'] ?? null; // Hämta filinformation
    $image_path = null; // Sökväg till sparad bild

    // Validering
    if (empty($title)) {
        $errors[] = 'Titel är obligatoriskt.';
    }
    if (empty($body)) {
        $errors[] = 'Innehåll är obligatoriskt.';
    }

    // ---- Bildhantering ----
    if ($image && $image['error'] === UPLOAD_ERR_OK) {
        // Validera filtyp och storlek (exempel)
        $allowed_types = ['image/jpeg', 'image/png', 'image/gif'];
        $max_size = 7 * 1024 * 1024; // 7 MB

        if (!in_array($image['type'], $allowed_types)) {
            $errors[] = 'Ogiltig filtyp. Endast JPG, PNG och GIF är tillåtna.';
        } elseif ($image['size'] > $max_size) {
            $errors[] = 'Filen är för stor. Maxstorlek är 5 MB.';
        } else {
            // Skapa ett unikt filnamn för att undvika kollisioner
            $file_extension = pathinfo($image['name'], PATHINFO_EXTENSION);
            $unique_filename = uniqid('post_img_', true) . '.' . $file_extension;
            $destination = UPLOAD_PATH . $unique_filename; // UPLOAD_PATH från config.php

            // Försök flytta filen till uploads/-mappen
            if (move_uploaded_file($image['tmp_name'], $destination)) {
                $image_path = 'uploads/' . $unique_filename; // Spara relativ sökväg för webbåtkomst
            } else {
                $errors[] = 'Kunde inte ladda upp bilden. Kontrollera mapprättigheter.';
                error_log("File Upload Error: Could not move file to " . $destination);
            }
        }
    } elseif ($image && $image['error'] !== UPLOAD_ERR_NO_FILE) {
        // Om ett annat fel än "ingen fil" inträffade
        $errors[] = 'Ett fel uppstod vid bilduppladdning. Felkod: ' . $image['error'];
    }
    // ---- Slut Bildhantering ----


    // Om inga valideringsfel, försök spara inlägget
    if (empty($errors)) {
        try {
            $pdo = connect_db();

            $stmt = $pdo->prepare("INSERT INTO posts (user_id, title, body, image_path) VALUES (:user_id, :title, :body, :image_path)");
            $stmt->bindParam(':user_id', $logged_in_user_id); // Använd ID från sessionen
            $stmt->bindParam(':title', $title);
            $stmt->bindParam(':body', $body);
            // Bind image_path, blir NULL om ingen bild laddades upp korrekt
            $stmt->bindParam(':image_path', $image_path, $image_path === null ? PDO::PARAM_NULL : PDO::PARAM_STR);


            if ($stmt->execute()) {
                // Inlägget skapades! Omdirigera till admin-dashboarden.
                header('Location: index.php?created=success');
                exit;
            } else {
                $errors[] = 'Ett fel uppstod när inlägget skulle sparas.';
                 // Om bilden hann sparas men DB-insert misslyckades, kan man överväga att ta bort bilden här.
                 if ($image_path && file_exists(UPLOAD_PATH . basename($image_path))) {
                     unlink(UPLOAD_PATH . basename($image_path));
                 }
            }
        } catch (PDOException $e) {
            error_log("Create Post Error: " . $e->getMessage());
            $errors[] = 'Databasfel. Kan inte spara inlägg just nu.';
            // Om bilden hann sparas men DB-insert misslyckades pga exception
            if ($image_path && file_exists(UPLOAD_PATH . basename($image_path))) {
                 unlink(UPLOAD_PATH . basename($image_path));
            }
        }
    }
}
?>
<!DOCTYPE html>
<html lang="sv">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Skapa nytt inlägg - Admin</title>
    <style> /* Enkel CSS */
        body { font-family: sans-serif; line-height: 1.6; padding: 20px; }
        .form-group { margin-bottom: 15px; }
        label { display: block; margin-bottom: 5px; }
        input[type="text"], textarea {
            width: 100%; padding: 8px; border: 1px solid #ccc; box-sizing: border-box;
        }
        textarea { min-height: 150px; }
        button { padding: 10px 15px; background-color: #28a745; color: white; border: none; cursor: pointer; }
        button:hover { background-color: #218838; }
        .error-messages { color: red; margin-bottom: 15px; }
        .error-messages ul { list-style: none; padding: 0; }
        a { color: #007bff; text-decoration: none; }
        a:hover { text-decoration: underline; }
    </style>
</head>
<body>
    <h1>Skapa nytt blogginlägg</h1>
    <p><a href="index.php">&laquo; Tillbaka till Admin Dashboard</a></p>

    <?php if (!empty($errors)): ?>
        <div class="error-messages">
            <strong>Inlägget kunde inte sparas:</strong>
            <ul>
                <?php foreach ($errors as $error): ?>
                    <li><?php echo htmlspecialchars($error); ?></li>
                <?php endforeach; ?>
            </ul>
        </div>
    <?php endif; ?>

    <!-- Viktigt: enctype="multipart/form-data" för filuppladdning -->
    <form action="create_post.php" method="post" enctype="multipart/form-data">
        <div class="form-group">
            <label for="title">Titel:</label>
            <input type="text" id="title" name="title" value="<?php echo htmlspecialchars($title); ?>" required>
        </div>
        <div class="form-group">
            <label for="body">Innehåll:</label>
            <textarea id="body" name="body" required><?php echo htmlspecialchars($body); ?></textarea>
        </div>
        <div class="form-group">
            <label for="image">Bild (valfritt, max 7MB, JPG/PNG/GIF):</label>
            <input type="file" id="image" name="image" accept="image/jpeg, image/png, image/gif">
        </div>
        <button type="submit">Spara inlägg</button>
    </form>

</body>
</html>

Förklaring av admin/create_post.php:

  1. Session Check: Koden från avsnitt 6 ser till att endast inloggade användare kan komma åt sidan. $logged_in_user_id sparas.
  2. Formulärhantering: Liknar register.php, men hämtar även filinformation från $_FILES['image'].
  3. Bildhantering:
    • Kollar om en fil har laddats upp ($image) och om det inte uppstod något fel under uppladdningen ($image['error'] === UPLOAD_ERR_OK).
    • Validering: Kontrollerar filtyp ($image['type']) mot en lista av tillåtna typer och filstorlek ($image['size']) mot en maxgräns.
    • Unikt Filnamn: Genererar ett unikt filnamn med uniqid() och pathinfo() för att undvika att filer skriver över varandra.
    • Destination: Skapar den fullständiga sökvägen till målmappen (UPLOAD_PATH från config.php + det unika filnamnet).
    • move_uploaded_file(): Försöker flytta den temporärt uppladdade filen ($image['tmp_name']) till den slutgiltiga destinationen. Detta är det säkra sättet att hantera filuppladdningar.
    • Spara Sökväg: Om flytten lyckas, sparas den relativa sökvägen (t.ex. uploads/bild.jpg) i $image_path. Denna relativa sökväg används sedan i <img>-taggar i HTML.
    • Felhantering: Om något går fel (fel filtyp, för stor fil, misslyckad flytt), läggs ett fel till i $errors-arrayen.
  4. Spara i Databas:
    • Om validering och eventuell bilduppladdning lyckades, körs en INSERT-fråga med prepared statements.
    • user_id hämtas från sessionen ($logged_in_user_id).
    • image_path binds. Viktigt: Om $image_path är null (ingen bild laddades upp eller det blev fel), måste vi specificera PDO::PARAM_NULL när vi binder, annars försöker PDO binda det som en tom sträng, vilket kan vara fel.
  5. Omdirigering/Fel: Omdirigera vid succé, visa fel annars. Om databasinfogningen misslyckas efter att bilden sparats, bör man försöka ta bort den sparade bilden (unlink()) för att undvika skräpfiler.
  6. HTML-formulär:
    • enctype="multipart/form-data": Detta attribut är absolut nödvändigt i <form>-taggen för att filuppladdning ska fungera.
    • <input type="file">: Fältet för att välja en bild. accept-attributet hjälper webbläsaren att filtrera vilka filer som visas.

8. Visa Blogginlägg (Read)

Lista Alla Inlägg (index.php)

Startsidan ska visa en lista över alla publicerade blogginlägg, kanske med titel, en kort del av innehållet, och en länk för att läsa mer.

index.php:

<?php
require_once 'includes/config.php';
require_once 'includes/database.php';

$posts = []; // Array för att lagra inlägg
$fetch_error = null;

try {
    $pdo = connect_db();
    // Hämta alla inlägg, sorterade med det senaste först
    // Hämta även användarnamnet för författaren via en JOIN
    $stmt = $pdo->query("SELECT posts.*, users.username
                         FROM posts
                         JOIN users ON posts.user_id = users.id
                         ORDER BY posts.created_at DESC");
    $posts = $stmt->fetchAll();

} catch (PDOException $e) {
    error_log("Index Page Error: " . $e->getMessage());
    $fetch_error = "Kunde inte hämta blogginlägg just nu. Försök igen senare.";
}

?>
<!DOCTYPE html>
<html lang="sv">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Enkel Blogg</title>
    <style>
        body { font-family: sans-serif; line-height: 1.6; padding: 20px; }
        .post-summary { border: 1px solid #eee; padding: 15px; margin-bottom: 20px; }
        .post-summary h2 { margin-top: 0; }
        .post-meta { font-size: 0.9em; color: #666; margin-bottom: 10px; }
        .post-image-list { max-width: 150px; max-height: 100px; float: right; margin-left: 15px; }
        .read-more { display: inline-block; margin-top: 10px; }
        nav { margin-bottom: 20px; background-color: #f8f9fa; padding: 10px; border-radius: 5px; }
        nav a { margin-right: 15px; text-decoration: none; color: #007bff; }
        nav a:hover { text-decoration: underline; }
        .error-message { color: red; border: 1px solid red; padding: 10px; margin-bottom: 20px; }
        .success-message { color: green; border: 1px solid green; padding: 10px; margin-bottom: 20px; }
    </style>
</head>
<body>

    <nav>
        <a href="index.php">Hem</a>
        <?php if (isset($_SESSION['user_id'])): ?>
            <a href="admin/index.php">Admin Dashboard</a>
            <a href="logout.php">Logga ut (<?php echo htmlspecialchars($_SESSION['username']); ?>)</a>
        <?php else: ?>
            <a href="login.php">Logga in</a>
            <a href="register.php">Registrera dig</a>
        <?php endif; ?>
    </nav>

    <h1>Välkommen till Bloggen!</h1>

    <?php if (isset($_GET['logged_out']) && $_GET['logged_out'] === 'success'): ?>
        <p class="success-message">Du har loggats ut.</p>
    <?php endif; ?>

    <?php if ($fetch_error): ?>
        <p class="error-message"><?php echo htmlspecialchars($fetch_error); ?></p>
    <?php else: ?>
        <?php if (empty($posts)): ?>
            <p>Det finns inga blogginlägg ännu.</p>
        <?php else: ?>
            <?php foreach ($posts as $post): ?>
                <article class="post-summary">
                    <?php if (!empty($post['image_path'])): ?>
                        <img src="<?php echo htmlspecialchars(BASE_URL . '/' . $post['image_path']); ?>"
                             alt="Inläggsbild" class="post-image-list">
                    <?php endif; ?>
                    <h2><?php echo htmlspecialchars($post['title']); ?></h2>
                    <div class="post-meta">
                        Publicerad: <?php echo date('Y-m-d H:i', strtotime($post['created_at'])); ?>
                        av <?php echo htmlspecialchars($post['username']); ?>
                    </div>
                    <p>
                        <?php
                        // Visa en kort del av texten (t.ex. 200 tecken)
                        $summary = htmlspecialchars($post['body']);
                        if (strlen($summary) > 200) {
                            $summary = substr($summary, 0, 200) . '...';
                        }
                        echo nl2br($summary); // nl2br gör om nya rader till <br>
                        ?>
                    </p>
                    <a href="post.php?id=<?php echo $post['id']; ?>" class="read-more">Läs mer &raquo;</a>
                    <div style="clear: both;"></div> <!-- Rensa float -->
                </article>
            <?php endforeach; ?>
        <?php endif; ?>
    <?php endif; ?>

</body>
</html>

Förklaring av index.php:

  1. Hämta Data:
    • Ansluter till databasen.
    • Använder $pdo->query() för en enkel SELECT-fråga (eftersom inga användarparametrar behövs här är query() okej, men prepare/execute fungerar också).
    • Använder JOIN users ON posts.user_id = users.id för att hämta författarens användarnamn (users.username) tillsammans med post-datan.
    • Sorterar med ORDER BY posts.created_at DESC för att visa de senaste inläggen först.
    • $stmt->fetchAll() hämtar alla matchande rader som en array av associativa arrayer.
    • Felhantering med try...catch.
  2. Navigation (<nav>): Visar olika länkar beroende på om användaren är inloggad (isset($_SESSION['user_id'])). Visar användarnamnet om inloggad.
  3. Fel/Succé-meddelanden: Visar meddelande om utloggning lyckats, eller om det blev fel vid hämtning av inlägg.
  4. Loopa igenom Inlägg:
    • Om inga fel och $posts-arrayen inte är tom, loopar vi igenom den med foreach.
    • För varje $post:
      • Visar bilden om image_path finns. Notera användningen av BASE_URL för att skapa en absolut sökväg till bilden.
      • Visar titel, metadata (datum, författare). strtotime() konverterar databasens tidsstämpel till en Unix timestamp som date() kan formatera.
      • Visar en förkortad version av brödtexten (substr) och använder nl2br() för att respektera radbrytningar i texten.
      • Skapar en "Läs mer"-länk till post.php och skickar med inläggets id i URL:en (?id=...).
    • htmlspecialchars() används genomgående för att skydda mot XSS när data från databasen skrivs ut.

Visa Enskilt Inlägg (post.php)

Denna sida visar hela innehållet för ett specifikt blogginlägg, baserat på id:t som skickas i URL:en.

post.php:

<?php
require_once 'includes/config.php';
require_once 'includes/database.php';

// Hämta post-ID från URL-parametern
$post_id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
$post = null;
$fetch_error = null;

if ($post_id === false || $post_id <= 0) {
    $fetch_error = "Ogiltigt inläggs-ID.";
} else {
    try {
        $post = get_post_by_id($post_id); // Använd funktionen!
        if (!$post) {
            $fetch_error = "Inlägget hittades inte.";
        }
    } catch (PDOException $e) {
         error_log("View Post Error (ID: $post_id): " . $e->getMessage());
         $fetch_error = "Kunde inte hämta blogginlägget just nu.";
    }
}

?>
<!DOCTYPE html>
<html lang="sv">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <!-- Titel sätts dynamiskt om inlägget hittas -->
    <title><?php echo $post ? htmlspecialchars($post['title']) : 'Inlägg'; ?> - Enkel Blogg</title>
     <style>
        body { font-family: sans-serif; line-height: 1.6; padding: 20px; }
        .post-content { margin-top: 20px; }
        .post-meta { font-size: 0.9em; color: #666; margin-bottom: 10px; }
        .post-image-full { max-width: 100%; height: auto; margin-bottom: 20px; }
        nav { margin-bottom: 20px; background-color: #f8f9fa; padding: 10px; border-radius: 5px; }
        nav a { margin-right: 15px; text-decoration: none; color: #007bff; }
        nav a:hover { text-decoration: underline; }
        .error-message { color: red; border: 1px solid red; padding: 10px; margin-bottom: 20px; }
    </style>
</head>
<body>

    <nav>
        <a href="index.php">Hem</a>
        <?php if (isset($_SESSION['user_id'])): ?>
            <a href="admin/index.php">Admin Dashboard</a>
            <a href="logout.php">Logga ut (<?php echo htmlspecialchars($_SESSION['username']); ?>)</a>
        <?php else: ?>
            <a href="login.php">Logga in</a>
            <a href="register.php">Registrera dig</a>
        <?php endif; ?>
    </nav>

    <?php if ($fetch_error): ?>
        <p class="error-message"><?php echo htmlspecialchars($fetch_error); ?></p>
    <?php elseif ($post): ?>
        <article class="post-content">
            <h1><?php echo htmlspecialchars($post['title']); ?></h1>
            <div class="post-meta">
                Publicerad: <?php echo date('Y-m-d H:i', strtotime($post['created_at'])); ?>
                av <?php echo htmlspecialchars($post['username']); ?>
                <?php if ($post['created_at'] !== $post['updated_at']): ?>
                    (Senast ändrad: <?php echo date('Y-m-d H:i', strtotime($post['updated_at'])); ?>)
                <?php endif; ?>
            </div>

             <?php if (!empty($post['image_path'])): ?>
                <img src="<?php echo htmlspecialchars(BASE_URL . '/' . $post['image_path']); ?>"
                     alt="<?php echo htmlspecialchars($post['title']); ?>" class="post-image-full">
            <?php endif; ?>

            <div>
                <?php
                // Skriv ut brödtexten, konvertera nya rader till <br>
                echo nl2br(htmlspecialchars($post['body']));
                ?>
            </div>
        </article>
        <hr>
        <p><a href="index.php">&laquo; Tillbaka till alla inlägg</a></p>
    <?php else: ?>
         <p class="error-message">Inlägget kunde inte laddas.</p> <!-- Fallback om post är null utan error -->
    <?php endif; ?>

</body>
</html>

Förklaring av post.php:

  1. Hämta ID: Använder filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT) för att säkert hämta och validera id-parametern från URL:en ($_GET['id']). Detta skyddar mot vissa typer av attacker och säkerställer att vi har ett heltal.
  2. Validera ID: Kontrollerar att $post_id är ett giltigt positivt heltal.
  3. Hämta Data:
    • Använder en prepared statement för att hämta inlägget med det specifika :id. Detta är viktigt även här för säkerhet, även om ID:t är validerat som heltal.
    • bindParam binder $post_id till platshållaren, och specificerar PDO::PARAM_INT för att vara extra tydlig med datatypen.
    • $stmt->fetch() hämtar den enda raden (eller false om ID inte finns)
  4. Felhantering: Sätter $fetch_error om ID är ogiltigt, inlägget inte hittas, eller om ett PDOException inträffar.
  5. Visa Innehåll: Om $post innehåller data (inte är null eller false), visas titel, metadata (inklusive "Senast ändrad" om updated_at skiljer sig från created_at), eventuell bild och hela brödtexten (nl2br(htmlspecialchars(...))).
  6. Navigation/Fel: Visar navigeringsmeny och eventuella felmeddelanden.

9. Redigera Blogginlägg (Update - admin/edit_post.php)

Denna sida låter en inloggad användare redigera sina egna inlägg. Den behöver både hämta befintlig data och hantera uppdateringar.

admin/edit_post.php:

<?php
require_once '../includes/config.php'; // Ger session_start()

// ---- Session Check ----
if (!isset($_SESSION['user_id'])) {
    header('Location: ../login.php?redirect=' . urlencode($_SERVER['REQUEST_URI']));
    exit;
}
$logged_in_user_id = $_SESSION['user_id'];
// ---- End Session Check ----

require_once '../includes/database.php';

$errors = [];
$post_id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
$post = null; // För att lagra den hämtade post-datan
$title = '';
$body = '';
$current_image_path = null;

// --- Steg 1: Hämta inlägget som ska redigeras ---
if ($post_id === false || $post_id <= 0) {
    $errors[] = "Ogiltigt inläggs-ID.";
} else {
    try {
        $pdo = connect_db();
        $stmt = $pdo->prepare("SELECT * FROM posts WHERE id = :id");
        $stmt->bindParam(':id', $post_id, PDO::PARAM_INT);
        $stmt->execute();
        $post = $stmt->fetch();

        if (!$post) {
             $errors[] = "Inlägget hittades inte.";
        } elseif ($post['user_id'] !== $logged_in_user_id) {
             $errors[] = "Du har inte behörighet att redigera detta inlägg.";
             $post = null; // Nollställ post så att formuläret inte visas
        } else {
            // Fyll i formulärvärden med hämtad data
            $title = $post['title'];
            $body = $post['body'];
            $current_image_path = $post['image_path'];
        }
    } catch (PDOException $e) {
        error_log("Edit Post Fetch Error (ID: $post_id): " . $e->getMessage());
        $errors[] = 'Databasfel. Kan inte hämta inlägg för redigering.';
        $post = null; // Nollställ post vid databasfel
    }
}
// --- Slut Steg 1 ---

// --- Steg 2: Hantera formulärdata vid POST (uppdatering) ---
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $post) { // Kör bara om post hämtades OK
    $title = trim($_POST['title'] ?? '');
    $body = trim($_POST['body'] ?? '');
    $image = $_FILES['image'] ?? null;
    $delete_image = isset($_POST['delete_image']); // Kolla om kryssrutan är ikryssad
    $new_image_path = $current_image_path; // Börja med den gamla bilden

    // Validering (samma som create)
    if (empty($title)) {
        $errors[] = 'Titel är obligatoriskt.';
    }
    if (empty($body)) {
        $errors[] = 'Innehåll är obligatoriskt.';
    }

    $image_uploaded = false; // Flagga för att se om ny bild laddats upp

    // ---- Bildhantering (liknar create, men med delete-option) ----
    if ($delete_image) {
        // Om användaren vill ta bort bilden
        if ($current_image_path && file_exists(UPLOAD_PATH . basename($current_image_path))) {
            if (unlink(UPLOAD_PATH . basename($current_image_path))) {
                $new_image_path = null; // Ta bort sökvägen
                 $current_image_path = null; // Uppdatera även för visning
            } else {
                 $errors[] = 'Kunde inte ta bort den befintliga bilden.';
                 error_log("File Deletion Error: Could not delete " . UPLOAD_PATH . basename($current_image_path));
            }
        } else {
            $new_image_path = null; // Ingen bild fanns eller kunde inte tas bort
            $current_image_path = null;
        }
    } elseif ($image && $image['error'] === UPLOAD_ERR_OK) {
        // Om en NY bild laddas upp (validera etc.)
        $allowed_types = ['image/jpeg', 'image/png', 'image/gif'];
        $max_size = 5 * 1024 * 1024; // 5 MB

        if (!in_array($image['type'], $allowed_types)) {
            $errors[] = 'Ogiltig filtyp för ny bild. Endast JPG, PNG och GIF.';
        } elseif ($image['size'] > $max_size) {
            $errors[] = 'Den nya filen är för stor. Maxstorlek är 5 MB.';
        } else {
            $file_extension = pathinfo($image['name'], PATHINFO_EXTENSION);
            $unique_filename = uniqid('post_img_', true) . '.' . $file_extension;
            $destination = UPLOAD_PATH . $unique_filename;

            if (move_uploaded_file($image['tmp_name'], $destination)) {
                // Ta bort den GAMLA bilden om den fanns
                if ($current_image_path && file_exists(UPLOAD_PATH . basename($current_image_path))) {
                    unlink(UPLOAD_PATH . basename($current_image_path));
                }
                $new_image_path = 'uploads/' . $unique_filename; // Spara nya sökvägen
                $image_uploaded = true; // Markera att ny bild laddats upp
            } else {
                $errors[] = 'Kunde inte ladda upp den nya bilden.';
                error_log("File Upload Error (Edit): Could not move file to " . $destination);
            }
        }
    } elseif ($image && $image['error'] !== UPLOAD_ERR_NO_FILE) {
        $errors[] = 'Ett fel uppstod vid bilduppladdning. Felkod: ' . $image['error'];
    }
    // ---- Slut Bildhantering ----

    // Om inga fel hittills, försök uppdatera databasen
    if (empty($errors)) {
        try {
            // Vi behöver $pdo igen här om det inte redan är definierat
            if (!isset($pdo)) $pdo = connect_db();

            $stmt = $pdo->prepare("UPDATE posts SET title = :title, body = :body, image_path = :image_path WHERE id = :id AND user_id = :user_id");
            $stmt->bindParam(':title', $title);
            $stmt->bindParam(':body', $body);
            $stmt->bindParam(':image_path', $new_image_path, $new_image_path === null ? PDO::PARAM_NULL : PDO::PARAM_STR);
            $stmt->bindParam(':id', $post_id, PDO::PARAM_INT);
            $stmt->bindParam(':user_id', $logged_in_user_id, PDO::PARAM_INT); // Dubbelkolla ägarskap vid UPDATE


            if ($stmt->execute()) {
                 // Uppdatering lyckades! Omdirigera till admin-dashboarden.
                header('Location: index.php?updated=success&id=' . $post_id);
                exit;
            } else {
                $errors[] = 'Ett fel uppstod när inlägget skulle uppdateras.';
                // Om en ny bild hann laddas upp men DB-update misslyckades, ta bort den nya bilden
                if ($image_uploaded && $new_image_path && file_exists(UPLOAD_PATH . basename($new_image_path))) {
                    unlink(UPLOAD_PATH . basename($new_image_path));
                }
            }

        } catch (PDOException $e) {
            error_log("Update Post Error (ID: $post_id): " . $e->getMessage());
            $errors[] = 'Databasfel. Kan inte uppdatera inlägg just nu.';
             // Om en ny bild hann laddas upp men DB-update misslyckades pga exception
             if ($image_uploaded && $new_image_path && file_exists(UPLOAD_PATH . basename($new_image_path))) {
                 unlink(UPLOAD_PATH . basename($new_image_path));
             }
        }
    }
}
// --- Slut Steg 2 ---

?>
<!DOCTYPE html>
<html lang="sv">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Redigera inlägg - Admin</title>
     <style> /* Samma CSS */
        body { font-family: sans-serif; line-height: 1.6; padding: 20px; }
        .form-group { margin-bottom: 15px; }
        label { display: block; margin-bottom: 5px; }
        input[type="text"], textarea {
            width: 100%; padding: 8px; border: 1px solid #ccc; box-sizing: border-box;
        }
        textarea { min-height: 150px; }
        button { padding: 10px 15px; background-color: #007bff; color: white; border: none; cursor: pointer; }
        button:hover { background-color: #0056b3; }
        .error-messages { color: red; margin-bottom: 15px; }
        .error-messages ul { list-style: none; padding: 0; }
        a { color: #007bff; text-decoration: none; }
        a:hover { text-decoration: underline; }
        .current-image img { max-width: 200px; max-height: 150px; display: block; margin-top: 5px; }
        .delete-image label { display: inline-block; margin-left: 5px; }
    </style>
</head>
<body>
    <h1>Redigera blogginlägg</h1>
    <p><a href="index.php">&laquo; Tillbaka till Admin Dashboard</a></p>

    <?php if (!empty($errors)): ?>
        <div class="error-messages">
            <strong>Inlägget kunde inte uppdateras (eller hämtas):</strong>
            <ul>
                <?php foreach ($errors as $error): ?>
                    <li><?php echo htmlspecialchars($error); ?></li>
                <?php endforeach; ?>
            </ul>
        </div>
    <?php endif; ?>

    <?php // Visa bara formuläret om inlägget kunde hämtas och användaren har behörighet ?>
    <?php if ($post): ?>
        <form action="edit_post.php?id=<?php echo $post_id; ?>" method="post" enctype="multipart/form-data">
            <div class="form-group">
                <label for="title">Titel:</label>
                <input type="text" id="title" name="title" value="<?php echo htmlspecialchars($title); ?>" required>
            </div>
            <div class="form-group">
                <label for="body">Innehåll:</label>
                <textarea id="body" name="body" required><?php echo htmlspecialchars($body); ?></textarea>
            </div>

            <div class="form-group">
                <label>Nuvarande Bild:</label>
                <?php if ($current_image_path): ?>
                    <div class="current-image">
                        <img src="<?php echo htmlspecialchars(BASE_URL . '/' . $current_image_path); ?>" alt="Nuvarande bild">
                    </div>
                    <div class="delete-image">
                         <input type="checkbox" id="delete_image" name="delete_image" value="1">
                         <label for="delete_image">Ta bort nuvarande bild</label>
                    </div>
                <?php else: ?>
                    <p>Ingen bild är uppladdad.</p>
                <?php endif; ?>
            </div>

            <div class="form-group">
                <label for="image">Ladda upp ny bild (valfritt, ersätter nuvarande):</label>
                <input type="file" id="image" name="image" accept="image/jpeg, image/png, image/gif">
            </div>

            <button type="submit">Uppdatera inlägg</button>
        </form>
    <?php endif; ?>

</body>
</html>

Förklaring av admin/edit_post.php:

  1. Session Check: Som tidigare.
  2. Hämta Inlägg (Steg 1):
    • Hämtar id från $_GET.
    • Använder prepared statement för att hämta inlägget med det ID:t.
    • Säkerhetskontroll: Verifierar att inlägget finns ($post) OCH att user_id i inlägget matchar ID:t för den inloggade användaren ($logged_in_user_id). Detta förhindrar att en användare redigerar någon annans inlägg genom att gissa ID:n. Om kontrollen misslyckas, sätts $post till null och ett fel läggs till.
    • Om allt är okej, fylls variablerna $title, $body, $current_image_path med data från databasen. Dessa används för att för-fylla formuläret.
  3. Hantera Uppdatering (Steg 2 - POST):
    • Koden körs bara om REQUEST_METHOD är POST OCH om $post hämtades framgångsrikt i steg 1.
    • Hämtar data från $_POST och $_FILES.
    • Kollar om kryssrutan delete_image är ikryssad.
    • Bildlogik:
      • Om delete_image är ikryssad: Försöker ta bort den gamla bildfilen med unlink() och sätter $new_image_path till null.
      • Om en ny bild laddas upp ($image finns och är OK): Validerar filen, flyttar den till uploads/, tar bort den gamla bilden om den fanns, och sätter $new_image_path till den nya sökvägen.
      • Om ingen ny bild laddas upp och delete_image inte är ikryssad, behålls $new_image_path = $current_image_path.
    • Validering: Validerar titel och brödtext.
    • Uppdatera Databas:
      • Om inga fel, kör en UPDATE-fråga med prepared statements.
      • Uppdaterar title, body och image_path (som nu kan vara den gamla, null eller en ny sökväg).
      • Viktigt: Inkluderar WHERE id = :id AND user_id = :user_id för att säkerställa att vi bara uppdaterar rätt inlägg OCH att den inloggade användaren fortfarande äger det (som en extra säkerhetsåtgärd).
    • Omdirigering vid succé, felhantering annars (inklusive att ta bort en nyligen uppladdad bild om DB-uppdateringen misslyckas).
  4. HTML-formulär:
    • Visas endast om $post hämtades framgångsrikt (och ägs av användaren).
    • Action-URL:en inkluderar ?id=... så att vi vet vilket inlägg som ska uppdateras när formuläret skickas.
    • Fälten är för-ifyllda med htmlspecialchars($title) och htmlspecialchars($body).
    • Visar den nuvarande bilden om den finns.
    • Inkluderar en kryssruta (delete_image) för att ta bort bilden.
    • Har ett fält för att ladda upp en ny bild.

10. Ta bort Blogginlägg (Delete - admin/delete_post.php)

Denna sida hanterar endast logiken för att ta bort ett inlägg. Den bör anropas via en POST-förfrågan (från en knapp i admin-listan) för att undvika oavsiktlig radering via GET-länkar.

admin/delete_post.php:

<?php
require_once '../includes/config.php'; // Ger session_start()

// ---- Session Check ----
if (!isset($_SESSION['user_id'])) {
    header('Location: ../login.php'); // Omdirigera direkt vid ej inloggad
    exit;
}
$logged_in_user_id = $_SESSION['user_id'];
// ---- End Session Check ----

require_once '../includes/database.php';

$errors = [];
$post_id = null;

// Radera endast om det är en POST-förfrågan
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $post_id = filter_input(INPUT_POST, 'post_id', FILTER_VALIDATE_INT);

    if ($post_id === false || $post_id <= 0) {
        $errors[] = "Ogiltigt inläggs-ID för radering.";
    } else {
        try {
            $pdo = connect_db();

            // Steg 1: Hämta bildsökväg INNAN radering (och kontrollera ägarskap)
            $stmt_fetch = $pdo->prepare("SELECT user_id, image_path FROM posts WHERE id = :id");
            $stmt_fetch->bindParam(':id', $post_id, PDO::PARAM_INT);
            $stmt_fetch->execute();
            $post_data = $stmt_fetch->fetch();

            if (!$post_data) {
                 $errors[] = "Inlägget som ska raderas hittades inte.";
            } elseif ($post_data['user_id'] !== $logged_in_user_id) {
                 $errors[] = "Du har inte behörighet att radera detta inlägg.";
            } else {
                // Användaren äger inlägget, fortsätt med radering

                // Steg 2: Försök radera posten från databasen
                $stmt_delete = $pdo->prepare("DELETE FROM posts WHERE id = :id AND user_id = :user_id");
                $stmt_delete->bindParam(':id', $post_id, PDO::PARAM_INT);
                $stmt_delete->bindParam(':user_id', $logged_in_user_id, PDO::PARAM_INT); // Dubbelkolla ägarskap

                if ($stmt_delete->execute()) {
                    // Radering från DB lyckades

                    // Steg 3: Försök ta bort den associerade bilden (om den finns)
                    $image_to_delete = $post_data['image_path'];
                    if ($image_to_delete && file_exists(UPLOAD_PATH . basename($image_to_delete))) {
                        if (!unlink(UPLOAD_PATH . basename($image_to_delete))) {
                            // Logga felet, men omdirigera ändå eftersom posten är borta från DB
                            error_log("File Deletion Error (Post Delete): Could not delete " . UPLOAD_PATH . basename($image_to_delete));
                            // Kanske lägga ett meddelande i sessionen för admin att se?
                            // $_SESSION['admin_notice'] = "Inlägg $post_id raderat, men kunde inte ta bort bildfil.";
                        }
                    }

                    // Omdirigera till admin-dashboarden med success-meddelande
                    header('Location: index.php?deleted=success&id=' . $post_id);
                    exit;

                } else {
                    $errors[] = "Kunde inte radera inlägget från databasen.";
                }
            }

        } catch (PDOException $e) {
            error_log("Delete Post Error (ID: $post_id): " . $e->getMessage());
            $errors[] = 'Databasfel. Kan inte radera inlägg just nu.';
        }
    }
} else {
    // Om inte POST, omdirigera bort (eller visa fel)
    $errors[] = "Ogiltig förfrågan för att radera inlägg.";
    // Kanske omdirigera? header('Location: index.php'); exit;
}

// Om vi når hit har något gått fel. Visa felmeddelanden (eller omdirigera med fel i URL)
// Detta är en förenklad felhantering för en ren backend-sida.
// I en mer avancerad app skulle man kanske sätta felmeddelande i sessionen
// och omdirigera tillbaka till index.php.
?>
<!DOCTYPE html>
<html lang="sv">
<head>
    <meta charset="UTF-8">
    <title>Raderingsfel - Admin</title>
     <style> /* Enkel CSS */
        body { font-family: sans-serif; line-height: 1.6; padding: 20px; }
        .error-messages { color: red; margin-bottom: 15px; border: 1px solid red; padding: 10px; }
        .error-messages ul { list-style: none; padding: 0; }
        a { color: #007bff; text-decoration: none; }
        a:hover { text-decoration: underline; }
    </style>
</head>
<body>
    <h1>Fel vid radering</h1>
     <?php if (!empty($errors)): ?>
        <div class="error-messages">
            <strong>Inlägget kunde inte raderas:</strong>
            <ul>
                <?php foreach ($errors as $error): ?>
                    <li><?php echo htmlspecialchars($error); ?></li>
                <?php endforeach; ?>
            </ul>
        </div>
    <?php endif; ?>
     <p><a href="index.php">&laquo; Tillbaka till Admin Dashboard</a></p>
</body>
</html>

Förklaring av admin/delete_post.php:

  1. Session Check & POST Check: Säkerställer inloggning och att skriptet anropas via POST.
  2. Hämta ID: Hämtar post_id från $_POST.
  3. Hämta Bildsökväg & Kontrollera Ägarskap:
    • INNAN vi raderar från databasen, hämtar vi inläggets user_id och image_path.
    • Vi kontrollerar att inlägget finns och att den inloggade användaren äger det.
  4. Radera från Databas:
    • Om ägarskapet är korrekt, kör en DELETE-fråga med prepared statements. Inkluderar AND user_id = :user_id som en extra säkerhetsåtgärd.
  5. Radera Bildfil:
    • Om raderingen från databasen lyckades (execute() returnerar true), försöker vi ta bort den associerade bildfilen med unlink().
    • Vi kollar först om $image_to_delete inte är null och om filen faktiskt existerar.
    • Om unlink() misslyckas loggar vi felet, men fortsätter (eftersom posten redan är borta från databasen).
  6. Omdirigering: Omdirigerar till admin/index.php med ett success-meddelande.
  7. Felhantering: Om något steg misslyckas (fel ID, ej ägare, DB-fel), samlas fel i $errors. Sidan visar sedan dessa fel. I en mer sofistikerad app skulle man kanske lagra felet i sessionen och omdirigera tillbaka till admin-listan.

11. Adminpanel (admin/index.php)

Detta är huvudsidan för inloggade användare där de kan se sina inlägg och hantera dem.

admin/index.php:

<?php
require_once '../includes/config.php'; // Ger session_start()

// ---- Session Check ----
if (!isset($_SESSION['user_id'])) {
    header('Location: ../login.php?redirect=' . urlencode($_SERVER['REQUEST_URI']));
    exit;
}
$logged_in_user_id = $_SESSION['user_id'];
$logged_in_username = $_SESSION['username'];
// ---- End Session Check ----

require_once '../includes/database.php';

$posts = []; // Array för användarens inlägg
$fetch_error = null;
$success_message = null; // För att visa meddelanden från create/update/delete

// Kolla efter success-meddelanden från omdirigeringar
if (isset($_GET['created']) && $_GET['created'] === 'success') {
    $success_message = "Nytt inlägg skapat!";
} elseif (isset($_GET['updated']) && $_GET['updated'] === 'success') {
    $success_message = "Inlägg (ID: " . htmlspecialchars($_GET['id'] ?? '') . ") uppdaterat!";
} elseif (isset($_GET['deleted']) && $_GET['deleted'] === 'success') {
     $success_message = "Inlägg (ID: " . htmlspecialchars($_GET['id'] ?? '') . ") raderat!";
}

try {
    $pdo = connect_db();
    // Hämta ENDAST den inloggade användarens inlägg
    $stmt = $pdo->prepare("SELECT id, title, created_at, updated_at
                           FROM posts
                           WHERE user_id = :user_id
                           ORDER BY created_at DESC");
    $stmt->bindParam(':user_id', $logged_in_user_id, PDO::PARAM_INT);
    $stmt->execute();
    $posts = $stmt->fetchAll();

} catch (PDOException $e) {
    error_log("Admin Index Error (User: $logged_in_user_id): " . $e->getMessage());
    $fetch_error = "Kunde inte hämta dina blogginlägg just nu.";
}

?>
<!DOCTYPE html>
<html lang="sv">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Admin Dashboard - Enkel Blogg</title>
    <style>
        body { font-family: sans-serif; line-height: 1.6; padding: 20px; }
        table { width: 100%; border-collapse: collapse; margin-top: 20px; }
        th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
        th { background-color: #f2f2f2; }
        .actions a, .actions button { margin-right: 5px; padding: 3px 6px; text-decoration: none; border: 1px solid transparent; }
        .actions .edit-btn { background-color: #ffc107; color: black; border-color: #ffc107; }
        .actions .delete-btn { background-color: #dc3545; color: white; border: none; cursor: pointer; font-size: inherit; font-family: inherit;}
        .actions .edit-btn:hover { background-color: #e0a800; }
        .actions .delete-btn:hover { background-color: #c82333; }
        nav { margin-bottom: 20px; background-color: #f8f9fa; padding: 10px; border-radius: 5px; }
        nav a { margin-right: 15px; text-decoration: none; color: #007bff; }
        nav a:hover { text-decoration: underline; }
        .create-link { display: inline-block; margin-bottom: 20px; background-color: #28a745; color: white; padding: 10px 15px; text-decoration: none; border-radius: 4px; }
        .create-link:hover { background-color: #218838; }
        .error-message { color: red; border: 1px solid red; padding: 10px; margin-bottom: 20px; }
        .success-message { color: green; border: 1px solid green; padding: 10px; margin-bottom: 20px; }
    </style>
</head>
<body>

    <nav>
        <a href="../index.php">Visa Blogg</a>
        <a href="index.php">Admin Dashboard</a>
        <a href="../logout.php">Logga ut (<?php echo htmlspecialchars($logged_in_username); ?>)</a>
    </nav>

    <h1>Admin Dashboard</h1>
    <p>Välkommen, <?php echo htmlspecialchars($logged_in_username); ?>!</p>

    <a href="create_post.php" class="create-link">Skapa nytt inlägg</a>

    <?php if ($success_message): ?>
        <p class="success-message"><?php echo htmlspecialchars($success_message); ?></p>
    <?php endif; ?>

    <?php if ($fetch_error): ?>
        <p class="error-message"><?php echo htmlspecialchars($fetch_error); ?></p>
    <?php else: ?>
        <h2>Dina Inlägg</h2>
        <?php if (empty($posts)): ?>
            <p>Du har inte skapat några inlägg ännu.</p>
        <?php else: ?>
            <table>
                <thead>
                    <tr>
                        <th>Titel</th>
                        <th>Skapad</th>
                        <th>Senast ändrad</th>
                        <th>Åtgärder</th>
                    </tr>
                </thead>
                <tbody>
                    <?php foreach ($posts as $post): ?>
                        <tr>
                            <td><?php echo htmlspecialchars($post['title']); ?></td>
                            <td><?php echo date('Y-m-d H:i', strtotime($post['created_at'])); ?></td>
                            <td><?php echo date('Y-m-d H:i', strtotime($post['updated_at'])); ?></td>
                            <td class="actions">
                                <a href="../post.php?id=<?php echo $post['id']; ?>" target="_blank">Visa</a>
                                <a href="edit_post.php?id=<?php echo $post['id']; ?>" class="edit-btn">Redigera</a>
                                <!-- Delete-knappen i ett eget formulär för POST -->
                                <form action="delete_post.php" method="post" style="display: inline;" onsubmit="return confirm('Är du säker på att du vill radera inlägget '<?php echo htmlspecialchars(addslashes($post['title'])); ?>'?');">
                                    <input type="hidden" name="post_id" value="<?php echo $post['id']; ?>">
                                    <button type="submit" class="delete-btn">Radera</button>
                                </form>
                            </td>
                        </tr>
                    <?php endforeach; ?>
                </tbody>
            </table>
        <?php endif; ?>
    <?php endif; ?>

</body>
</html>

Förklaring av admin/index.php:

  1. Hämta Data:
    • Ansluter till databasen.
    • Använder $pdo->query() för en enkel SELECT-fråga (eftersom inga användarparametrar behövs här är query() okej, men prepare/execute fungerar också).
    • Använder JOIN users ON posts.user_id = users.id för att hämta författarens användarnamn (users.username) tillsammans med post-datan.
    • Sorterar med ORDER BY posts.created_at DESC för att visa de senaste inläggen först.
    • $stmt->fetchAll() hämtar alla matchande rader som en array av associativa arrayer.
    • Felhantering med try...catch.
  2. Navigation (<nav>): Visar olika länkar beroende på om användaren är inloggad (isset($_SESSION['user_id'])). Visar användarnamnet om inloggad.
  3. Fel/Succé-meddelanden: Visar meddelande om utloggning lyckats, eller om det blev fel vid hämtning av inlägg.
  4. Loopa igenom Inlägg:
    • Om inga fel och inlägg finns, visas en HTML-tabell.
    • Loopar igenom $posts.
    • Varje rad visar titel, datum och åtgärdsknappar:
      • "Visa": Länk till den publika post.php.
      • "Redigera": Länk till edit_post.php med rätt id.
      • "Radera": Detta är en knapp inuti ett litet formulär. Formuläret skickar post_id via POST till delete_post.php. Detta är säkrare än en enkel GET-länk för radering. onsubmit="return confirm(...)" lägger till en JavaScript-bekräftelsedialog innan formuläret skickas. addslashes() används inuti JavaScript-strängen för att hantera eventuella citationstecken i titeln.

Nu har vi en fungerande, om än mycket enkel, bloggapplikation med grundläggande CRUD, autentisering, sessioner och bildhantering.

12. Refaktorering och Förbättringar

Koden ovan fungerar, men den är väldigt blandad (HTML, PHP-logik, databasfrågor i samma fil). I en större applikation blir detta snabbt ohanterligt. Här är några steg vi kan ta för att förbättra den:

a) Separera Logik med Funktioner

Vi kan flytta återkommande kodblock, särskilt databasinteraktioner, till funktioner i includes/functions.php.

Exempel (skapa includes/functions.php):

<?php
// includes/functions.php
require_once 'database.php'; // Behövs för att anropa connect_db()

/**
 * Hämtar ett specifikt inlägg från databasen.
 *
 * @param int $post_id ID för inlägget som ska hämtas.
 * @return array|false Post-datan som en associativ array, eller false om den inte hittas.
 * @throws PDOException Vid databasfel.
 */
function get_post_by_id(int $post_id): array|false {
    $pdo = connect_db();
    $stmt = $pdo->prepare("SELECT posts.*, users.username
                           FROM posts
                           JOIN users ON posts.user_id = users.id
                           WHERE posts.id = :id");
    $stmt->bindParam(':id', $post_id, PDO::PARAM_INT);
    $stmt->execute();
    return $stmt->fetch();
}

/**
 * Hämtar alla inlägg av en specifik användare.
 *
 * @param int $user_id Användarens ID.
 * @return array En array med alla användarens inlägg.
 * @throws PDOException Vid databasfel.
 */
function get_posts_by_user(int $user_id): array {
     $pdo = connect_db();
     $stmt = $pdo->prepare("SELECT id, title, created_at, updated_at
                            FROM posts
                            WHERE user_id = :user_id
                            ORDER BY created_at DESC");
     $stmt->bindParam(':user_id', $user_id, PDO::PARAM_INT);
     $stmt->execute();
     return $stmt->fetchAll();
}

// Lägg till fler funktioner för:
// - get_all_posts()
// - create_post(...)
// - update_post(...)
// - delete_post(...)
// - get_user_by_username(...)
// - create_user(...)
// etc.

?>

Användning (t.ex. i post.php):

<?php
require_once 'includes/config.php';
// require_once 'includes/database.php'; // Behövs inte direkt om funktionen inkluderar den
require_once 'includes/functions.php'; // Inkludera funktionerna

$post_id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
$post = null;
$fetch_error = null;

if ($post_id === false || $post_id <= 0) {
    $fetch_error = "Ogiltigt inläggs-ID.";
} else {
    try {
        $post = get_post_by_id($post_id); // Använd funktionen!
        if (!$post) {
            $fetch_error = "Inlägget hittades inte.";
        }
    } catch (PDOException $e) {
         error_log("View Post Error (ID: $post_id): " . $e->getMessage());
         $fetch_error = "Kunde inte hämta blogginlägget just nu.";
    }
}

// Resten av HTML-koden...
?>

b) PHP Type Hinting

Genom att lägga till typdeklarationer för funktionsparametrar och returvärden (som i functions.php-exemplet ovan) kan vi göra koden tydligare och fånga vissa typer av fel tidigare.

  • int $post_id: Förväntar sig ett heltal som parameter.
  • : array|false: Funktionen förväntas returnera antingen en array eller false.
  • : void: Om en funktion inte returnerar något.

c) Förbättrad Filstruktur (Exempel)

En vanlig förbättring är att separera HTML-presentationen från PHP-logiken med hjälp av "template"-filer.

.
├── config/
│   └── config.php
│   └── database.php
├── public/              <-- Web root (här pekar webbservern)
│   ├── admin/
│   │   └── index.php      # Hanterar logik, inkluderar template
│   │   └── create_post.php
│   │   └── edit_post.php
│   │   └── delete_post.php
│   ├── css/
│   │   └── style.css
│   ├── uploads/         # Fortfarande här, tillgänglig via webben
│   ├── index.php        # Hanterar logik, inkluderar template
│   ├── login.php
│   ├── logout.php
│   ├── post.php
│   └── register.php
├── src/                 # PHP-kod (inte direkt åtkomlig via webben)
│   └── functions.php    # Eller klasser för Posts, Users etc.
└── templates/           # HTML-mallar
    ├── admin/
    │   ├── index.view.php
    │   ├── create_post.view.php
    │   └── edit_post.view.php
    ├── partials/        # Återanvändbara delar
    │   ├── header.view.php
    │   ├── footer.view.php
    │   └── nav.view.php
    ├── index.view.php
    ├── login.view.php
    ├── post.view.php
    └── register.view.php

I denna struktur skulle t.ex. public/index.php innehålla PHP-koden för att hämta data och sedan inkludera templates/index.view.php för att visa den. templates/index.view.php skulle i sin tur inkludera partials/header.view.php etc.

d) Ytterligare Förbättringar att Överväga

  • Routing: Istället för att ha en PHP-fil för varje URL, använda en "front controller" (index.php) och ett routing-bibliotek som dirigerar förfrågningar till rätt kod baserat på URL:en.
  • Templating Engines: Använda ett mallhanteringssystem som Twig för att ytterligare separera HTML och PHP och erbjuda funktioner som mall-arv.
  • Objektorienterad Programmering (OOP): Skapa klasser för User och Post för att kapsla in data och beteende.
  • Dependency Injection: Hantera beroenden (som databasanslutningen) på ett mer flexibelt sätt.
  • Säkerhet:
    • CSRF (Cross-Site Request Forgery) Tokens: Lägga till dolda tokens i formulär (särskilt för POST-operationer som create, update, delete) för att förhindra CSRF-attacker.
    • XSS (Cross-Site Scripting) Prevention: Vara ännu noggrannare med att sanera all output med htmlspecialchars() eller ett kontextmedvetet mallhanteringssystem.
    • File Upload Security: Striktare validering av filtyper (inte bara lita på $_FILES['type']), eventuellt byta namn på filer, lagra filer utanför webbroten om möjligt.
  • Felhantering: Mer sofistikerad felhantering och loggning.

Denna lektion har gett en grundläggande men fungerande CRUD-applikation. Genom att bygga vidare på refaktoreringsidéerna kan du skapa mer robusta och underhållbara PHP-applikationer.

</rewritten_file>

Sessioner och Cookies: Att Komma Ihåg Användare

En av de grundläggande utmaningarna i webbutveckling är att HTTP (Hypertext Transfer Protocol), protokollet som webbläsare och servrar använder för att kommunicera, är stateless (tillståndslöst). Varje HTTP-förfrågan (request) från en webbläsare till en server behandlas som en helt oberoende händelse. Servern har inget inbyggt minne av tidigare förfrågningar från samma användare.

Detta är ett problem när vi vill bygga funktioner som kräver att servern "kommer ihåg" användaren mellan sidladdningar, till exempel:

  • Att hålla en användare inloggad.
  • Att spara innehållet i en kundvagn i en e-handel.
  • Att komma ihåg användarens preferenser (t.ex. språkval).

För att lösa detta behöver vi mekanismer för state management (tillståndshantering) över flera HTTP-förfrågningar. De två vanligaste teknikerna för detta i webbutveckling är cookies (kakor) och sessions (sessioner).

Vad är Cookies?

En cookie är en liten textbaserad datafil som en webbserver kan be webbläsaren att spara på användarens dator. När webbläsaren sedan gör framtida förfrågningar till samma webbserver, skickar den automatiskt med de sparade cookies som är relevanta för den servern.

Hur det fungerar:

  1. Server skickar cookie: Servern inkluderar en Set-Cookie-header i sitt HTTP-svar till webbläsaren.
    HTTP/1.1 200 OK
    Content-Type: text/html
    Set-Cookie: username=KalleAnka; expires=Fri, 31 Dec 2025 23:59:59 GMT; path=/; HttpOnly
    
    <!-- HTML-innehåll -->
    
  2. Webbläsare sparar cookie: Webbläsaren tar emot svaret och sparar informationen från Set-Cookie (namn, värde, utgångsdatum etc.).
  3. Webbläsare skickar cookie: Vid nästa förfrågan till samma domän och sökväg inkluderar webbläsaren en Cookie-header med de sparade kakorna.
    GET /profile.php HTTP/1.1
    Host: www.example.com
    Cookie: username=KalleAnka
    
  4. Server läser cookie: Servern (PHP i vårt fall) kan nu läsa värdet från Cookie-headern.

Hantera Cookies i PHP

  • Sätta en cookie (setcookie()):

    <?php
    // setcookie(name, value, expire, path, domain, secure, httponly);
    
    // Sätt en cookie som heter 'user_preference' med värdet 'dark_mode'
    // Giltig i 30 dagar (86400 sekunder/dag * 30)
    $expiry_time = time() + (86400 * 30);
    setcookie('user_preference', 'dark_mode', $expiry_time, '/'); // path='/' gör den giltig på hela sajten
    
    // Sätt en cookie som bara ska skickas över HTTPS och inte vara åtkomlig via JavaScript
    setcookie('session_token', 'abc123xyz', 0, '/', '', true, true);
    // expire = 0 betyder att den raderas när webbläsaren stängs (session cookie)
    ?>
    

    Viktigt: setcookie() måste anropas innan någon HTML eller annan output skickas till webbläsaren, eftersom den modifierar HTTP-headers.

  • Läsa en cookie ($_COOKIE): Cookies som skickats av webbläsaren finns tillgängliga i den superglobala arrayen $_COOKIE.

    <?php
    // Kolla om en cookie finns och hämta dess värde
    if (isset($_COOKIE['user_preference'])) {
        $preference = $_COOKIE['user_preference'];
        echo "Din sparade preferens är: " . htmlspecialchars($preference);
    } else {
        echo "Ingen preferens sparad.";
    }
    ?>
    
  • Ta bort en cookie: Sätt en ny cookie med samma namn, men med ett utgångsdatum i det förflutna.

    <?php
    // Ta bort 'user_preference'-cookien
    setcookie('user_preference', '', time() - 3600, '/'); 
    ?>
    

För- och Nackdelar med Cookies

  • Fördelar: Enkla att använda för att spara små mängder icke-känslig data på klientsidan, kan ha lång livslängd.
  • Nackdelar:
    • Säkerhet: Data lagras på användarens dator och kan läsas och potentiellt manipuleras.
    • Integritet: Används ofta för spårning mellan webbplatser (tredjepartscookies), vilket lett till striktare regler (GDPR) och webbläsarinställningar.
    • Storleksbegränsning: Webbläsare har gränser för antal cookies per domän och total storlek (ofta runt 4KB per cookie).
    • Skickas med varje request: Kan öka mängden data som skickas i onödan.

Vad är Sessioner?

Sessioner är ett sätt att lagra användarspecifik information på servern under tiden användaren interagerar med webbplatsen. Istället för att lagra all data i webbläsaren (som med cookies), lagrar man bara en unik identifierare (ett Session ID) i webbläsaren, oftast via en cookie.

Hur det fungerar (typiskt flöde):

  1. Användare besöker sidan: Första gången en användare startar en session (eller om ingen giltig session-ID finns) gör PHP följande:
    • Genererar ett unikt Session ID (en lång, slumpmässig sträng).
    • Skapar en fil (eller databaspost) på servern associerad med detta ID för att lagra sessionsdata.
    • Skickar Session ID:t till webbläsaren via en Set-Cookie-header (oftast med namnet PHPSESSID).
  2. Webbläsare skickar Session ID: Vid efterföljande förfrågningar skickar webbläsaren tillbaka Session ID-cookien.
  3. Server identifierar session: PHP tar emot Session ID:t från cookien, letar upp den associerade sessionfilen på servern och laddar dess innehåll in i den superglobala $_SESSION-arrayen.
  4. PHP använder sessionsdata: Skriptet kan nu läsa och skriva data till $_SESSION-arrayen.
  5. Data sparas: När PHP-skriptet avslutas sparas innehållet i $_SESSION-arrayen automatiskt tillbaka till sessionfilen på servern.
sequenceDiagram
    participant B as Webbläsare
    participant S as Server (PHP)
    participant Store as Session Store (Server)

    B->>+S: GET /sida.php (Ingen session cookie)
    S->>S: Generera unikt Session ID (t.ex. 'xyz789')
    S->>Store: Skapa lagring för session 'xyz789'
    S-->>B: Skicka HTML + Set-Cookie: PHPSESSID=xyz789
    B->>B: Spara cookie PHPSESSID=xyz789

    B->>+S: GET /annan_sida.php (Cookie: PHPSESSID=xyz789)
    S->>S: Identifiera session via PHPSESSID='xyz789'
    S->>Store: Ladda data för session 'xyz789'
    S->>S: PHP använder/modifierar $_SESSION
    S->>Store: Spara uppdaterad data för session 'xyz789'
    S-->>-B: Skicka HTML

Hantera Sessioner i PHP

  • Starta/Återuppta en session (session_start()): Denna funktion måste anropas i början av varje skript som behöver tillgång till sessionsdata, innan någon output skickas.

    <?php
    // Måste vara FÖRST i filen (eller innan någon output)
    session_start();
    
    // Nu kan vi använda $_SESSION
    // ...
    ?>
    <!DOCTYPE html>
    <html>
    ...
    

    Det är vanligt att inkludera session_start() i en gemensam konfigurationsfil (som görs i CRUD-app-exemplet) som inkluderas på alla sidor.

  • Lagra data ($_SESSION): $_SESSION är en superglobal associativ array där du kan lagra och hämta sessionsdata.

    <?php
    session_start();
    
    // Lagra användarens ID och namn
    $_SESSION['user_id'] = 123;
    $_SESSION['username'] = 'KalleAnka';
    
    // Lagra en enkel räknare
    if (!isset($_SESSION['page_views'])) {
        $_SESSION['page_views'] = 1;
    } else {
        $_SESSION['page_views']++;
    }
    
    echo "Du har besökt " . $_SESSION['page_views'] . " sidor under denna session.";
    ?>
    
  • Läsa data ($_SESSION): Du läser data precis som från vilken annan array som helst.

    <?php
    session_start();
    
    if (isset($_SESSION['username'])) {
        echo "Välkommen tillbaka, " . htmlspecialchars($_SESSION['username']) . "!";
    } else {
        echo "Välkommen, gäst!";
    }
    ?>
    
  • Ta bort sessionsdata:

    • Ta bort en specifik variabel: unset($_SESSION['variable_name']);
    • Ta bort all sessionsdata: $_SESSION = [];
  • Förstöra sessionen helt (session_destroy()): För att helt avsluta en session (t.ex. vid utloggning) behöver du göra flera saker:

    <?php
    session_start(); // Sessionen måste vara startad för att kunna förstöras
    
    // 1. Ta bort alla sessionsvariabler
    $_SESSION = [];
    
    // 2. Ta bort sessionscookien från webbläsaren
    if (ini_get("session.use_cookies")) {
        $params = session_get_cookie_params();
        setcookie(session_name(), '', time() - 42000,
            $params["path"], $params["domain"],
            $params["secure"], $params["httponly"]
        );
    }
    
    // 3. Förstör sessionen på servern (tar bort sessionfilen)
    session_destroy();
    
    echo "Du har loggats ut och sessionen är förstörd.";
    // Ofta följt av en omdirigering
    // header('Location: login.php');
    // exit;
    ?>
    

Sessioner vs. Cookies: När Använda Vad?

EgenskapCookiesSessioner
LagringsplatsAnvändarens webbläsare (Klienten)Webbservern
DataLiten mängd textdata (ca 4KB)Kan lagra komplex data (arrayer, objekt etc.)
SäkerhetLägre (kan läsas/ändras av klienten)Högre (data på servern, bara ID hos klienten)
LivslängdKan sättas till lång tid (dagar/år)Varar tills webbläsaren stängs (default) eller tills den "expirerar" på servern / förstörs manuellt.
AnvändningPreferenser, "Kom ihåg mig", spårningAnvändarlogin, kundvagnar, temporär data
BeroendeInget serverberoende (efter satt)Kräver serverresurser för lagring
IdentifieringKan användas för att identifiera sessionKräver en identifierare (oftast en cookie)

Tumregel:

  • Använd cookies för att lagra icke-känslig information som du vill ska finnas kvar länge, eller för att identifiera en session.
  • Använd sessioner för att lagra känslig eller temporär information som är knuten till en specifik användares besök, särskilt för inloggningsstatus.

Sessionshantering för Användarautentisering

Sessioner är den vanligaste metoden för att hantera användarinloggning på webbplatser byggda med PHP.

Inloggningsprocess (Konceptuellt):

  1. Användaren skickar användarnamn och lösenord (via ett POST-formulär).
  2. Servern (PHP) validerar input.
  3. Servern hämtar användarens data (inklusive den hashade lösenordet) från databasen baserat på användarnamnet.
  4. Servern använder password_verify() för att jämföra det inskickade lösenordet med det hashade lösenordet från databasen.
  5. Om giltigt: a. Anropa session_start(). b. Viktigt: Anropa session_regenerate_id(true); för att skapa ett nytt session-ID och ogiltigförklara det gamla. Detta skyddar mot Session Fixation-attacker, där en angripare försöker lura användaren att använda ett session-ID som angriparen känner till. c. Lagra nödvändig information i $_SESSION, framför allt användarens ID: $_SESSION['user_id'] = $user['id'];. d. Omdirigera användaren till en skyddad sida (t.ex. adminpanelen).
  6. Om ogiltigt: Visa ett felmeddelande.

Kontrollera inloggningsstatus på skyddade sidor:

<?php
session_start();

if (!isset($_SESSION['user_id'])) {
    // Inte inloggad, skicka till login
    header('Location: login.php');
    exit;
}

// Användaren är inloggad, fortsätt ladda sidan...
$user_id = $_SESSION['user_id'];
// ... hämta mer data baserat på $user_id om det behövs ...
?>

Utloggningsprocess: Som visat tidigare: starta sessionen, töm $_SESSION, ta bort cookien, förstör sessionen på servern, och omdirigera.

Säkerhetsaspekter för Sessioner

Eftersom sessioner ofta används för att hantera känslig information som inloggningsstatus, är det viktigt att konfigurera dem säkert.

  • Session Fixation: Förhindras främst genom att anropa session_regenerate_id(true) direkt efter en lyckad inloggning eller när användarens behörighetsnivå ändras.
  • Session Hijacking (Kapning): När en angripare lyckas stjäla en giltig användares session-ID. Detta kan ske genom:
    • Nätverksavlyssning: Om kommunikationen inte är krypterad (HTTP istället för HTTPS), kan ID:t snappas upp.
    • Cross-Site Scripting (XSS): Om en angripare kan köra JavaScript i offrets webbläsare kan de försöka läsa session-cookien (om den inte är skyddad).
    • Gissning (mycket osannolikt med moderna, långa, slumpmässiga ID:n).
  • Åtgärder mot Kapning:
    • Använd HTTPS: Krypterar all kommunikation, inklusive session-cookien.
    • HttpOnly Cookie Flag: Sätt session.cookie_httponly = 1 (i php.ini eller via ini_set). Detta förhindrar JavaScript från att komma åt cookien, vilket minskar risken vid XSS.
    • Secure Cookie Flag: Sätt session.cookie_secure = 1. Detta säkerställer att session-cookien endast skickas över HTTPS-anslutningar. Absolut nödvändigt för produktionsmiljöer.
    • SameSite Cookie Attribute: Sätt session.cookie_samesite = "Lax" (bra standard) eller "Strict". Detta hjälper till att skydda mot Cross-Site Request Forgery (CSRF)-attacker genom att kontrollera när cookien skickas med förfrågningar från andra webbplatser.

Konfigurera Sessionssäkerhet

Dessa inställningar görs bäst i din php.ini-fil för att gälla globalt. Om du inte har tillgång till den, kan de ibland sättas i början av dina skript med ini_set() (innan session_start() anropas), men det är mindre idealiskt.

Rekommenderade inställningar i php.ini (eller motsvarande):

; Använd bara cookies för att sprida session ID (säkrare)
session.use_only_cookies = 1

; Skicka bara session cookie över HTTPS
session.cookie_secure = 1

; Gör session cookie otillgänglig för JavaScript
session.cookie_httponly = 1

; Sätt SameSite attributet (Lax är en bra balans)
session.cookie_samesite = "Lax"

; Använd starkare hash-algoritm för session ID (om tillgängligt)
; session.sid_entropy_length = 32 (eller högre)
; session.sid_bits_per_character = 5 (eller 6)

Sammanfattning

Cookies och sessioner är avgörande verktyg för att skapa dynamiska och användarvänliga webbapplikationer genom att överbrygga HTTP:s tillståndslösa natur. Cookies lagrar data på klienten och är bra för icke-känsliga preferenser, medan sessioner lagrar data på servern och är standardmetoden för att hantera inloggningsstatus och annan känslig temporär data. Att förstå hur man använder dem säkert, särskilt genom att skydda session-ID:t och konfigurera cookie-attribut korrekt, är fundamentalt för säker webbutveckling med PHP.

Webbsäkerhet i PHP: Vanliga Hot och Skydd

Att bygga en fungerande webbapplikation är en sak, men att göra den säker är en helt annan och minst lika viktig uppgift. En osäker applikation kan leda till att användardata läcker, att obehöriga får kontroll över systemet, eller att webbplatsen används för att sprida skadlig kod. Detta avsnitt täcker några av de mest kritiska säkerhetsriskerna och hur du skyddar din PHP-applikation mot dem.

1. SQL Injection (SQL-injektion)

Vad är det? SQL Injection är en av de mest kända och farligaste attackerna. Den inträffar när en angripare lyckas "injicera" (infoga) skadlig SQL-kod i en databasfråga via osanerad användarinput (t.ex. från ett formulärfält eller en URL-parameter). Om applikationen bygger ihop SQL-frågan genom att direkt klistra in denna input, kan angriparen manipulera frågan till att göra saker den inte borde, som att:

  • Hämta känslig data (andra användares lösenord, kreditkortsinformation).
  • Modifiera eller radera data.
  • I vissa fall, få kontroll över hela databasservern.

Exempel på Sårbar Kod:

Anta att du vill hämta en användare baserat på ett ID från en URL (login.php?id=123).

<?php
// MYCKET OSÄKERT EXEMPEL - ANVÄND ALDRIG SÅ HÄR!
require_once 'includes/database.php';

$user_id = $_GET['id']; // Hämta ID direkt från URL
$pdo = connect_db();

// Bygger ihop SQL-frågan genom att klistra in användarinput direkt!
$sql = "SELECT username, email FROM users WHERE id = " . $user_id;

echo "Exekverar: " . $sql . "\n";

// Kör den osäkra frågan
// $stmt = $pdo->query($sql); 
// $user = $stmt->fetch();
// ... visa användardata ...
?>

Problem: Vad händer om en angripare anropar sidan med login.php?id=123 OR 1=1?

SQL-frågan blir då: SELECT username, email FROM users WHERE id = 123 OR 1=1

Villkoret OR 1=1 är alltid sant, så frågan kommer att returnera alla användare i databasen, inte bara användare 123!

Ännu värre, om angriparen skickar login.php?id=123; DROP TABLE users; --

SQL-frågan (beroende på databas) kan bli: SELECT username, email FROM users WHERE id = 123; DROP TABLE users; --

Detta skulle kunna radera hela users-tabellen! (; separerar kommandon, -- startar en kommentar).

Hur man Skyddar Sig: Prepared Statements (Förberedda Uttryck)

Det absolut viktigaste skyddet mot SQL Injection är att aldrig klistra in användarinput direkt i SQL-frågor. Använd istället Prepared Statements, som vi gör genomgående i crud-app.md med PDO.

Så här fungerar det:

  1. Förbered Frågan: Skicka SQL-frågan till databasen med platshållare (: eller ?) istället för de faktiska värdena.
  2. Bind Parametrar: Tala om för databasen vilka variabler som ska användas för varje platshållare och vilken datatyp de har.
  3. Exekvera: Kör den förberedda frågan. Databasen sätter då in värdena på ett säkert sätt, och behandlar dem alltid som data, aldrig som SQL-kod.

Säker Kod (med PDO):

<?php
// SÄKERT EXEMPEL MED PREPARED STATEMENTS
require_once 'includes/database.php';

$user_id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT); // Validera först input!

if ($user_id) {
    try {
        $pdo = connect_db();

        // 1. Förbered frågan med platshållare
        $sql = "SELECT username, email FROM users WHERE id = :user_id";
        $stmt = $pdo->prepare($sql);

        // 2. Bind parametern (värdet från $user_id)
        $stmt->bindParam(':user_id', $user_id, PDO::PARAM_INT);

        // 3. Exekvera
        $stmt->execute();

        $user = $stmt->fetch();

        if ($user) {
            echo "Användare hittad: " . htmlspecialchars($user['username']);
        } else {
            echo "Användare inte hittad.";
        }

    } catch (PDOException | Exception $e) { // PDOException eller annan valideringsfel
        error_log("SQL Error: " . $e->getMessage());
        echo "Ett fel uppstod.";
    }
} else {
    echo "Ogiltigt ID.";
}
?>

Om en angripare nu försöker med ?id=123 OR 1=1, kommer filter_input att returnera false (eftersom det inte är ett heltal). Om de försöker med ?id=123; DROP TABLE users; --, även om det skulle passera filter_input (vilket det inte gör), skulle databasen behandla hela strängen "123; DROP TABLE users; --" som det värde som ska bindas till :userid (och troligen misslyckas med att hitta en match eller ge ett typfel), inte som separat SQL-kod.

Alltid använda Prepared Statements när användarinput är inblandad i SQL-frågor!

2. Cross-Site Scripting (XSS)

Vad är det? XSS inträffar när en angripare lyckas injicera skadlig kod (oftast JavaScript) i en webbsida som sedan visas för andra användare. När den andra användarens webbläsare renderar sidan, exekveras den skadliga koden inom kontexten av den legitima webbplatsen.

Detta kan användas för att:

  • Stjäla användarens session cookies (och därmed kapa deras session).
  • Omdirigera användaren till en falsk inloggningssida.
  • Modifiera innehållet på sidan användaren ser.
  • Utföra åtgärder i användarens namn.

Det vanligaste sättet XSS sker är när en webbapplikation tar emot data från en användare (t.ex. en kommentar, ett profilnamn, ett sökord) och sedan skriver ut denna data direkt i HTML-koden utan att sanera den först.

Exempel på Sårbar Kod:

Anta att du har en gästbok där användare kan lämna meddelanden.

<?php
// OSÄKERT EXEMPEL - VISAR ANVÄNDARINPUT DIREKT!

// Antag att $comment hämtats från databasen, där en användare skrivit:
// <script>alert('XSS Attack! Din cookie: ' + document.cookie);</script>
$comment = "<script>alert('XSS Attack! Din cookie: ' + document.cookie);</script>";

?>
<!DOCTYPE html>
<html>
<body>
    <h1>Kommentarer</h1>
    <div class="comment">
        <?php echo $comment; // SKRIVER UT SKADLIG KOD DIREKT! ?>
    </div>
</body>
</html>

När en annan användare besöker sidan, kommer deras webbläsare att se <script>...</script>-taggen och exekvera JavaScript-koden inuti den. I detta fall visas en popup med användarens cookies (potentiellt session-ID:t!).

Hur man Skyddar Sig: Sanera All Output (htmlspecialchars)

Den grundläggande regeln är: Lita aldrig på data som kommer utifrån (användarinput, data från databas som kan ha manipulerats etc.). Innan du skriver ut sådan data i ett HTML-sammanhang, måste du sanera den så att den behandlas som ren text och inte som HTML-kod.

Den vanligaste och viktigaste funktionen för detta i PHP är htmlspecialchars().

htmlspecialchars() konverterar specialtecken som har en HTML-betydelse till deras motsvarande HTML-entiteter:

  • & blir &amp;
  • " (dubbelt citationstecken) blir &quot; (om ENT_QUOTES är satt)
  • ' (enkelt citationstecken) blir &#039; eller &apos; (om ENT_QUOTES är satt)
  • < (mindre än) blir &lt;
  • > (större än) blir &gt;

Säker Kod:

<?php
// SÄKERT EXEMPEL - SANERAR OUTPUT

$comment = "<script>alert('XSS Attack! Din cookie: ' + document.cookie);</script>";

?>
<!DOCTYPE html>
<html>
<body>
    <h1>Kommentarer</h1>
    <div class="comment">
        <?php echo htmlspecialchars($comment, ENT_QUOTES, 'UTF-8'); ?>
    </div>
</body>
</html>

Nu kommer HTML-koden som webbläsaren tar emot att se ut ungefär så här:

<div class="comment">
    &lt;script&gt;alert(&#039;XSS Attack! Din cookie: &#039; + document.cookie);&lt;/script&gt;
</div>

Webbläsaren kommer att visa detta som text, men inte exekvera det som ett skript, eftersom < och > har omvandlats till ofarliga entiteter.

Argument till htmlspecialchars():

  • $string: Strängen som ska saneras.
  • $flags: Oftast ENT_QUOTES för att konvertera både enkel- och dubbelcitationstecken. Detta är viktigt om du skriver ut data inuti HTML-attribut (t.ex. value="...").
  • $encoding: Ange teckenkodningen, nästan alltid 'UTF-8' för moderna webbapplikationer.

Använd htmlspecialchars() konsekvent på ALL data som kommer utifrån innan den skrivs ut i HTML!

Not: Om du behöver skriva ut data i andra sammanhang (t.ex. inuti ett JavaScript-block eller ett CSS-värde) krävs andra typer av sanering/kodning. htmlspecialchars är specifikt för HTML-kroppen och HTML-attribut.

3. Password Hashing (Säker Lösenordshantering)

Vad är problemet? Att lagra användarnas lösenord i klartext i databasen är extremt oansvarigt. Om databasen blir stulen får angriparen tillgång till alla lösenord direkt.

Att använda enkla hash-funktioner som MD5 eller SHA1 är inte heller säkert längre. Dessa är:

  • Snabba: Gör dem lätta att knäcka med brute-force-attacker (testa miljontals lösenord per sekund).
  • Kollisionsbenägna (särskilt MD5): Olika lösenord kan ge samma hash.
  • Inte saltade (i sig själva): Samma lösenord ger alltid samma hash, vilket gör dem sårbara för "rainbow table"-attacker (förberäknade tabeller med hashvärden för vanliga lösenord).

Hur man Skyddar Sig: Starka, Saltade Hashar (password_hash / password_verify)

Vad är en hashning-funktion?

Innan vi dyker in i PHP:s funktioner, låt oss förstå vad hashing (hashning) innebär. En hashfunktion är en matematisk algoritm som tar en indata av valfri storlek (t.ex. ett lösenord) och omvandlar den till en utdata av fast storlek, kallad en hash eller ett hashvärde (ibland "digest").

Tänk dig en mixer:

  1. Du lägger i ingredienser (ditt lösenord, "mittLösenord123").
  2. Du kör mixern (hashfunktionen, t.ex. Bcrypt).
  3. Ut kommer en smoothie (hashen, t.ex. $2y$10$abcdef...xyz).

Viktiga egenskaper hos kryptografiska hashfunktioner (som används för lösenord):

  • Envägsfunktion (One-way): Det är extremt svårt (praktiskt taget omöjligt) att gå från smoothien (hashen) tillbaka till de ursprungliga ingredienserna (lösenordet). Du kan inte "köra mixern baklänges".
  • Deterministisk: Samma ingredienser (samma lösenord och samma salt, se nedan) i samma mixer (samma hashfunktion) ger alltid exakt samma smoothie (hash).
  • Snabb att beräkna: Det ska gå relativt fort att skapa hashen från lösenordet.
  • Kollisionsresistent: Det ska vara extremt svårt att hitta två olika indata (lösenord) som ger samma hash.
  • Lavineffekt (Avalanche effect): En liten ändring i indata (t.ex. ändra en bokstav i lösenordet) ska resultera i en helt annorlunda hash.

Varför inte bara hasha? Problemet med salt. Om vi bara använde en enkel hashfunktion (som gamla MD5 eller SHA1) skulle samma lösenord alltid ge samma hash. Angripare kan då använda "rainbow tables" – gigantiska listor med förberäknade hashar för vanliga lösenord. Om din lagrade hash finns i deras tabell, vet de omedelbart ditt lösenord.

Lösningen är saltning. Ett salt är en slumpmässig datasträng som läggs till lösenordet innan det hashas.

Liknelse: Tänk att du lägger till en unik, hemlig krydda (saltet) till dina ingredienser innan du mixar dem. Även om två personer använder exakt samma grundingredienser (lösenord), kommer deras smoothies (hashar) att bli helt olika eftersom de använde olika hemliga kryddor (salt).

Moderna funktioner som PHP:s password_hash():

  1. Genererar ett unikt, slumpmässigt salt för varje lösenord.
  2. Kombinerar saltet med lösenordet.
  3. Hashar kombinationen med en stark algoritm (som Bcrypt).
  4. Sparar både saltet och hashen tillsammans i en enda sträng i databasen.

När password_verify() används, extraherar den saltet från den lagrade strängen, kombinerar det med lösenordet som användaren skrev in, hashar det, och jämför sedan resultatet med den lagrade hashen.

graph LR
    subgraph Hashning utan salt
        P1["Lösenord: 'password123'"] --> H1(Hashfunktion MD5);
        P2["Lösenord: 'password123'"] --> H1;
        H1 --> Hash1["Hash: 'e8636ea0...' "];
        Hash1 --> DB1["Databas: lagrar 'e8636ea0...'"];
        Hash1 --> DB2["Databas: lagrar 'e8636ea0...'"];
    end

    subgraph Hashning med salt - password_hash
        P3["Lösenord: 'password123'"] --> S1(Generera Salt1: 'abc');
        S1 --> C1("Kombinera: 'abc' + 'password123'");
        C1 --> H2(Hashfunktion Bcrypt);
        H2 --> HashSalt1["Hash+Salt: '$2y$10$abc...'"];
        HashSalt1 --> DB3["Databas: lagrar '$2y$10$abc...'"];

        P4["Lösenord: 'password123'"] --> S2(Generera Salt2: 'xyz');
        S2 --> C2("Kombinera: 'xyz' + 'password123'");
        C2 --> H2;
        H2 --> HashSalt2["Hash+Salt: '$2y$10$xyz...'"];
        HashSalt2 --> DB4["Databas: lagrar '$2y$10$xyz...'"];
    end

    style P1 fill:#4ac
    style P2 fill:#4ac
    style DB1 fill:#4ac
    style DB2 fill:#4ac

Diagrammet illustrerar hur samma lösenord utan salt ger samma hash, medan password_hash med unika salt ger olika lagrade hashvärden för samma lösenord, vilket skyddar mot rainbow tables.

PHP har sedan version 5.5 inbyggda, moderna funktioner för säker lösenordshantering:

  • password_hash($password, PASSWORD_DEFAULT):

    • Tar ett lösenord i klartext ($password).
    • Använder en stark, långsam hash-algoritm (oftast Bcrypt som standard med PASSWORD_DEFAULT). Långsamheten gör brute-force mycket svårare.
    • Genererar automatiskt ett unikt, slumpmässigt salt för varje lösenord.
    • Kombinerar algoritmen, saltet och hashen till en enda sträng som är säker att lagra i databasen (t.ex. i en VARCHAR(255)-kolumn).
  • password_verify($password, $hash):

    • Tar det lösenord användaren anger vid inloggning ($password) och den lagrade hashen från databasen ($hash).
    • Extraherar automatiskt algoritmen och saltet från $hash.
    • Hashar det angivna lösenordet med samma algoritm och salt.
    • Jämför resultatet säkert (mot tidattacker) med den lagrade hashen.
    • Returnerar true om lösenordet matchar, annars false.

Exempel (som i crud-app.md):

<?php
// Vid registrering:
$passwordFromUser = "mittSäkraLösenord123";
$hashedPasswordForDatabase = password_hash($passwordFromUser, PASSWORD_DEFAULT);
// Spara $hashedPasswordForDatabase i users.password_hash

echo "Lagrad hash: " . $hashedPasswordForDatabase . "\n";
// Exempel output: $2y$10$abcdefghijklmnopqrstuvwx/ABCDEFGHIJKLMNOPQRSTU.abcdefghijklm

// Vid inloggning:
$passwordAttempt = "mittSäkraLösenord123"; // Lösenord från formulär
$hashFromDatabase = "$2y$10$abcdefghijklmnopqrstuvwx/ABCDEFGHIJKLMNOPQRSTU.abcdefghijklm"; // Hämtad från DB

if (password_verify($passwordAttempt, $hashFromDatabase)) {
    echo "Lösenordet är korrekt!";
} else {
    echo "Felaktigt lösenord.";
}

// Försök med fel lösenord:
$wrongPasswordAttempt = "felLösenord";
if (password_verify($wrongPasswordAttempt, $hashFromDatabase)) {
    echo "Korrekt?!"; // Körs inte
} else {
    echo "Felaktigt lösenord (som förväntat).";
}
?>

Använd alltid password_hash() och password_verify() för lösenord! PASSWORD_DEFAULT säkerställer att du automatiskt använder den bästa tillgängliga algoritmen som stöds av din PHP-version.

4. Cross-Site Request Forgery (CSRF)

Vad är det? CSRF är en attack där en angripare lurar en inloggad användares webbläsare att skicka en oönskad förfrågan till en webbapplikation där användaren är autentiserad. Angriparen kan inte se svaret, men kan få användarens webbläsare att utföra en handling.

Exempel: En användare är inloggad på sin blogg (myblog.com). De besöker sedan en skadlig webbplats (evil.com). På evil.com finns en dold bild eller ett formulär som automatiskt skickar en POST-förfrågan till myblog.com/admin/delete_post.php med post_id=42. Eftersom användaren är inloggad på myblog.com, skickar webbläsaren med den giltiga session-cookien, och delete_post.php tror att det är användaren själv som vill radera inlägget.

Detta fungerar eftersom webbläsare automatiskt skickar med relevanta cookies (som session-ID) för en domän, oavsett varifrån förfrågan initierades.

CSRF-attacker riktar sig främst mot state-changing requests (förfrågningar som ändrar data, t.ex. POST, PUT, DELETE), inte rena GET-förfrågningar som bara hämtar data.

Exempel på Sårbar Kod (Konceptuellt):

Om admin/delete_post.php bara kollar om användaren är inloggad och sedan raderar baserat på $_POST['post_id'] utan någon ytterligare kontroll, är den sårbar.

<?php
// SÅRBAR DELETE (utan CSRF-skydd)
require_once '../includes/config.php'; 
if (!isset($_SESSION['user_id'])) { exit; }
require_once '../includes/database.php';

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $post_id = filter_input(INPUT_POST, 'post_id', FILTER_VALIDATE_INT);
    $logged_in_user_id = $_SESSION['user_id'];
    
    if ($post_id) {
        try {
            $pdo = connect_db();
            // Kanske kontrolleras ägarskap, men ingen CSRF-token!
            $stmt = $pdo->prepare("DELETE FROM posts WHERE id = :id AND user_id = :user_id");
            $stmt->bindParam(':id', $post_id, PDO::PARAM_INT);
            $stmt->bindParam(':user_id', $logged_in_user_id, PDO::PARAM_INT);
            $stmt->execute();
            // ... omdirigering ...
        } catch (PDOException $e) { /* ... */ }
    }
}
?>

Hur man Skyddar Sig: Anti-CSRF Tokens (Synchronizer Token Pattern)

Den vanligaste metoden är att använda anti-CSRF tokens:

  1. Generera Token: När servern visar ett formulär som utför en state-changing action (t.ex. skapa, uppdatera, radera), genererar den en unik, slumpmässig, hemlig token (en sträng).
  2. Lagra Token: Servern lagrar denna token i användarens session ($_SESSION['csrf_token']).
  3. Skicka Token: Servern inkluderar samma token som ett dolt fält (<input type="hidden">) i HTML-formuläret som skickas till användarens webbläsare.
  4. Skicka tillbaka Token: När användaren skickar in formuläret, skickas den dolda token tillbaka till servern tillsammans med övrig formulärdata (i $_POST).
  5. Verifiera Token: På serversidan, innan den state-changing åtgärden utförs, jämför servern token som skickades med formuläret ($_POST['csrf_token']) med den token som är lagrad i sessionen ($_SESSION['csrf_token']).
    • Om de matchar, är det troligtvis en legitim förfrågan initierad av användaren via formuläret.
    • Om de inte matchar (eller om token saknas), avvisas förfrågan eftersom den kan vara en CSRF-attack (en angripare kan inte gissa eller läsa token i användarens session).

Exempel på Implementering (Konceptuellt):

Generera och visa formulär (t.ex. i admin/index.php för delete-knappen):

<?php
session_start(); // Eller via config.php

// Generera token om den inte redan finns i sessionen
if (empty($_SESSION['csrf_token'])) {
    $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); // Skapa en säker, slumpmässig token
}
$csrf_token = $_SESSION['csrf_token'];

// ... (kod för att lista inlägg) ...

// I loopen för varje post:
?>
<form action="delete_post.php" method="post" style="display: inline;" onsubmit="return confirm('...');">
    <input type="hidden" name="post_id" value="<?php echo $post['id']; ?>">
    <!-- Lägg till den dolda CSRF-token -->
    <input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrf_token); ?>">
    <button type="submit" class="delete-btn">Radera</button>
</form>
<?php 
// ... (resten av loopen) ...
?>

Verifiera på serversidan (t.ex. i admin/delete_post.php):

<?php
require_once '../includes/config.php'; // Ger session_start()
// ... (Session Check) ...
require_once '../includes/database.php';

$errors = [];

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    // Hämta token från POST och session
    $submitted_token = $_POST['csrf_token'] ?? '';
    $session_token = $_SESSION['csrf_token'] ?? ' '; // Använd blanksteg om ej satt för säker jämförelse

    // 1. Verifiera CSRF-token!
    if (!hash_equals($session_token, $submitted_token)) {
        // Token matchar inte eller saknas!
        $errors[] = "Ogiltig förfrågan (CSRF-skydd). Försök igen.";
        // Avbryt direkt!
    } else {
        // Token är OK, fortsätt med resten av logiken...
        $post_id = filter_input(INPUT_POST, 'post_id', FILTER_VALIDATE_INT);
        // ... (resten av delete-logiken: hämta bild, kontrollera ägarskap, radera) ...
    }
} else {
    $errors[] = "Endast POST-förfrågningar tillåtna.";
}

// ... (Visa fel eller omdirigera) ...
?>
  • bin2hex(random_bytes(32)) genererar en kryptografiskt säker slumpmässig token.
  • hash_equals($session_token, $submitted_token) är viktig för att jämföra tokens. Den utför en tidsoberoende jämförelse, vilket skyddar mot vissa typer av "timing attacks". Använd inte == eller === för att jämföra hemligheter som CSRF-tokens eller API-nycklar.

Implementera CSRF-skydd för alla formulär som utför state-changing actions!

5. Filuppladdningssäkerhet

Att tillåta användare att ladda upp filer medför risker:

  • Skadlig kod: En användare kan ladda upp en fil som innehåller server-side kod (t.ex. en .php-fil) som sedan kan exekveras på servern om den placeras i en webbåtkomlig mapp.
  • Resursutmattning: Användare kan ladda upp extremt stora filer som tar upp diskutrymme eller bandbredd.
  • XSS via filnamn/metadata: Ibland kan skadlig kod gömmas i filnamn eller filens metadata.

Grundläggande Skydd (som i crud-app.md):

  • Validera Filtyp: Kontrollera filens MIME-typ ($_FILES['image']['type']) mot en vitlista av tillåtna typer. Lita inte blint på detta, då det kan förfalskas av klienten. En mer robust metod är att använda filinnehållsanalys (t.ex. finfo_file i PHP).
  • Validera Filstorlek: Kontrollera $FILES['image']['size'] mot en rimlig maxgräns.
  • Byt Namn på Filen: Spara aldrig filen med det namn användaren angav. Generera ett unikt, slumpmässigt filnamn (som vi gjorde med uniqid()) för att undvika kollisioner och potentiellt skadliga filnamn.
  • Använd move_uploaded_file(): Detta är den enda säkra funktionen för att flytta en uppladdad fil från dess temporära plats till den slutgiltiga destinationen. Den utför extra kontroller.
  • Begränsa Mapprättigheter: Se till att mappen där filer sparas (uploads/) inte har exekveringsrättigheter för webbservern.

Ytterligare Förbättringar:

  • Lagra Filer Utanför Webbroten: Om möjligt, lagra uppladdade filer i en mapp som inte är direkt åtkomlig via webben. Skapa sedan ett PHP-skript som agerar mellanhand: det kontrollerar behörighet och skickar sedan ut filens innehåll med rätt Content-Type-header.
  • Skanna Filer: Använd antivirusprogram eller andra verktyg för att skanna uppladdade filer efter skadlig kod.

Slutsats

Webbsäkerhet är ett komplext och ständigt föränderligt område. De punkter vi tagit upp här – SQL Injection, XSS, säker lösenordshantering, CSRF och grundläggande filuppladdningssäkerhet – är några av de absolut viktigaste att ha koll på som PHP-utvecklare. Kom ihåg:

  • Filtrera input, sanera output.
  • Använd Prepared Statements för all SQL som involverar extern data.
  • Använd password_hash() och password_verify() för lösenord.
  • Skydda state-changing actions med Anti-CSRF tokens.
  • Var försiktig med filuppladdningar.
  • Håll din PHP-version och dina beroenden uppdaterade.
  • Använd alltid HTTPS i produktion.

Att tänka på säkerhet från början och genom hela utvecklingsprocessen är avgörande för att bygga pålitliga och säkra webbapplikationer.

Kapitel 7: Praktiska Övningar

Dessa övningar är tänkta att hjälpa dig att befästa kunskaperna från kapitel 7. Försök att lösa dem själv först innan du tittar på lösningsförslagen.

Övningar

PHP Syntax och Grundläggande

  1. Hej Världen: Skriv ett PHP-skript som skriver ut texten "Hej Världen!" till webbläsaren.
  2. Variabler och Utskrift: Deklarera en variabel $productName med värdet "Laptop" och en variabel $productPrice med värdet 999.90. Skriv ut en mening som "Produkten Laptop kostar 999.9 kr." med hjälp av dessa variabler och strängkonkatenering (.).
  3. if/else: Skriv en if/else-sats som kollar om variabeln $productPrice (från övning 2) är större än 500. Om den är det, skriv ut "Dyr produkt.", annars skriv ut "Billig produkt.".
  4. Funktion: Skapa en funktion calculateDiscount($price, $percentage) som tar ett pris och en procentsats (som heltal, t.ex. 10 för 10%) och returnerar det nya priset efter rabatten. Använd type hinting för parametrarna (float för pris, int för procent) och returvärdet (float).
  5. Anropa Funktion: Anropa funktionen calculateDiscount med priset 250.0 och procentsatsen 20. Skriv ut resultatet.
  6. match-uttryck: Skriv ett match-uttryck som tar en variabel $statusCode (anta att den innehåller ett heltal som 200, 404, eller 500) och returnerar en beskrivande sträng ("OK", "Not Found", "Server Error", eller "Unknown" för andra värden). Skriv ut den returnerade strängen.
  7. Strikta Typer: Hur aktiverar du strikt typläge i en PHP-fil, och varför är det rekommenderat?

Arrayer och Loopar

  1. Indexerad Array: Skapa en indexerad array $cities som innehåller strängarna "Stockholm", "Göteborg", "Malmö".
  2. Åtkomst: Skriv ut den första staden ("Stockholm") från arrayen $cities.
  3. Lägg till: Lägg till staden "Uppsala" i slutet av $cities-arrayen.
  4. count(): Skriv ut hur många städer som finns i $cities-arrayen.
  5. Associativ Array: Skapa en associativ array $car med nycklarna brand (värde "Volvo") och model (värde "XC60").
  6. Åtkomst (Associativ): Skriv ut bilens modell från $car-arrayen.
  7. foreach (Värden): Använd en foreach-loop för att skriva ut varje stad från $cities-arrayen på en ny rad.
  8. foreach (Nyckel & Värde): Använd en foreach-loop för att skriva ut både nyckel och värde från $car-arrayen, t.ex. "brand: Volvo".
  9. in_array(): Kontrollera om staden "Malmö" finns i $cities-arrayen och skriv ut "Ja" eller "Nej".
  10. implode(): Skapa en sträng från $cities-arrayen där städerna är separerade med kommatecken och mellanslag (", "). Skriv ut strängen.

SQL (Syntax)

Anta att du har en tabell customers med kolumnerna id (INT, PK), name (VARCHAR), email (VARCHAR), city (VARCHAR) och en tabell orders med kolumnerna order_id (INT, PK), customer_id (INT, FK till customers.id), order_date (DATE), amount (DECIMAL).

  1. SELECT Alla: Skriv SQL för att hämta alla kolumner för alla kunder.
  2. SELECT Specifika Kolumner & WHERE: Skriv SQL för att hämta name och email för kunder som bor i "Göteborg".
  3. ORDER BY & LIMIT: Skriv SQL för att hämta namnen på de 10 första kunderna sorterade i bokstavsordning (A-Ö).
  4. INSERT: Skriv SQL för att lägga till en ny kund med namn "Lisa Berg" och e-post "lisa@example.com" som bor i "Malmö".
  5. UPDATE: Skriv SQL för att ändra staden till "Lund" för kunden med id 15.
  6. DELETE: Skriv SQL för att ta bort kunden med id 20.
  7. INNER JOIN: Skriv SQL för att hämta order-ID (orders.order_id) och kundens namn (customers.name) för alla ordrar.

Sessioner och Säkerhet (Koncept/PHP)

  1. Starta Session: Vilken PHP-funktion används för att starta eller återuppta en session, och var i skriptet bör den placeras?
  2. Lagra i Session: Skriv PHP-koden för att lagra värdet "dark" i sessionsvariabeln theme.
  3. Kontrollera Session: Skriv en if-sats i PHP som kontrollerar om sessionsvariabeln user_id är satt.
  4. Förhindra XSS: Vilken PHP-funktion bör du alltid använda när du skriver ut data som kan ha kommit från en användare i ett HTML-sammanhang? Ge ett exempel.
  5. Förhindra SQL Injection: Vilken teknik bör du alltid använda när du bygger SQL-frågor som innehåller värden från användarinput? Nämn den PHP-funktion (i PDO) som initierar denna teknik.
  6. Lösenordshantering: Vilka två PHP-funktioner (introducerade i PHP 5.5) är standard för att hantera lösenord säkert, och vad gör de?
  7. CSRF-skydd: Vad är en vanlig metod för att skydda formulär mot Cross-Site Request Forgery (CSRF)-attacker?

Mini-CRUD (PHP/PDO)

Anta att du har en PDO-anslutning i variabeln $pdo och en tasks-tabell med kolumnerna id (INT, PK, AUTO_INCREMENT) och description (TEXT).

  1. Hämta Input: Skriv PHP-koden för att säkert hämta värdet från ett formulärfält med name="task_description" som skickats via POST.
  2. INSERT (PDO): Skriv PHP-koden som använder $pdo och Prepared Statements för att infoga värdet från föregående uppgift (lagrat i en variabel $description) i tasks-tabellens description-kolumn.
  3. SELECT Alla (PDO): Skriv PHP-koden som använder $pdo för att hämta alla rader från tasks-tabellen (endast id och description) och lagra resultatet i en array $allTasks.

Klasser (OOP Grunderna)

  1. Definiera Klass: Skapa en klass Book med två publika egenskaper: $title (string) och $author (string). Lägg till en publik metod displayInfo() som skriver ut "Title: [titel], Author: [författare]".
  2. Skapa Objekt: Skapa två olika Book-objekt från klassen ovan. Sätt olika värden för title och author för varje objekt. Anropa displayInfo()-metoden på båda objekten.
  3. Konstruktor: Modifiera Book-klassen så att den har en konstruktor (__construct) som tar emot titel och författare som argument och sätter egenskaperna direkt när objektet skapas. Skapa ett nytt Book-objekt med hjälp av konstruktorn och anropa displayInfo().
  4. Synlighet & Getters: Modifiera Book-klassen igen:
    • Gör egenskaperna $title och $author privata (private).
    • Lägg till en ny publik egenskap $pages (int).
    • Uppdatera konstruktorn så att den även tar emot antal sidor ($pages) och sätter alla tre egenskaperna.
    • Skapa publika "getter"-metoder: getTitle(), getAuthor(), och getPages() som returnerar värdet på respektive privat egenskap.
  5. Använda Getters: Skapa ett Book-objekt från den modifierade klassen i föregående uppgift. Använd getter-metoderna för att hämta och skriva ut bokens titel, författare och antal sidor.

Lösningsförslag

PHP Syntax och Grundläggande

  1. <?php
    echo "Hej Världen!";
    ?>
    

    eller

    <?= "Hej Världen!"; ?>
    
  2. <?php
    $productName = "Laptop";
    $productPrice = 999.90;
    echo "Produkten " . $productName . " kostar " . $productPrice . " kr.";
    // Alternativt med dubbla citationstecken för interpolering:
    // echo "Produkten $productName kostar $productPrice kr.";
    ?>
    
  3. <?php
    $productPrice = 999.90; // Från övning 2
    if ($productPrice > 500) {
        echo "Dyr produkt.";
    } else {
        echo "Billig produkt.";
    }
    ?>
    
  4. <?php
    declare(strict_types=1);
    function calculateDiscount(float $price, int $percentage): float {
        if ($percentage < 0 || $percentage > 100) {
            // Ohanterad procentsats, returnera ursprungspris eller kasta fel?
            throw new InvalidArugmentException('$percentage måste vara mellan 0-100')
        }
        $discountFactor = 1 - ($percentage / 100.0);
        return $price * $discountFactor;
    }
    ?>
    
  5. <?php
    // Antag att funktionen från övning 4 finns
    $discountedPrice = calculateDiscount(250.0, 20);
    echo "Pris efter 20% rabatt: " . $discountedPrice; // Output: Pris efter 20% rabatt: 200
    ?>
    
  6. <?php
    $statusCode = 404;
    $message = match ($statusCode) {
        200 => "OK",
        404 => "Not Found",
        500 => "Server Error",
        default => "Unknown",
    };
    echo "Status: " . $message; // Output: Status: Not Found
    ?>
    
  7. Strikt typläge aktiveras med declare(strict_types=1); överst i PHP-filen (efter <?php-taggen). Det rekommenderas för att PHP inte ska försöka tvångsomvandla datatyper mellan inkompatibla typer vid funktionsanrop, vilket fångar fel tidigare och gör koden mer förutsägbar och robust.

Arrayer och Loopar

  1. <?php
    $cities = ["Stockholm", "Göteborg", "Malmö"];
    // Eller äldre syntax: $cities = array("Stockholm", "Göteborg", "Malmö");
    ?>
    
  2. <?php
    $cities = ["Stockholm", "Göteborg", "Malmö"];
    echo $cities[0]; // Output: Stockholm
    ?>
    
  3. <?php
    $cities = ["Stockholm", "Göteborg", "Malmö"];
    $cities[] = "Uppsala"; // Lägger till i slutet
    // Alternativt: array_push($cities, "Uppsala");
    print_r($cities); // Visar hela arrayen för kontroll
    ?>
    
  4. <?php
    $cities = ["Stockholm", "Göteborg", "Malmö", "Uppsala"];
    echo count($cities); // Output: 4
    ?>
    
  5. <?php
    $car = [
        'brand' => 'Volvo',
        'model' => 'XC60'
    ];
    ?>
    
  6. <?php
    $car = ['brand' => 'Volvo', 'model' => 'XC60'];
    echo $car['model']; // Output: XC60
    ?>
    
  7. <?php
    $cities = ["Stockholm", "Göteborg", "Malmö", "Uppsala"];
    foreach ($cities as $city) {
        echo $city . "\n"; // \n för ny rad om det körs i terminalen, <br> för webbläsare
    }
    ?>
    
  8. <?php
    $car = ['brand' => 'Volvo', 'model' => 'XC60'];
    foreach ($car as $key => $value) {
        echo $key . ": " . $value . "\n";
    }
    ?>
    
  9. <?php
    $cities = ["Stockholm", "Göteborg", "Malmö", "Uppsala"];
    if (in_array("Malmö", $cities)) {
        echo "Ja";
    } else {
        echo "Nej";
    }
    ?>
    
  10. <?php
    $cities = ["Stockholm", "Göteborg", "Malmö", "Uppsala"];
    $cityString = implode(", ", $cities);
    echo $cityString; // Output: Stockholm, Göteborg, Malmö, Uppsala
    ?>
    

SQL (Syntax)

  1. SELECT * FROM customers;
  2. SELECT name, email FROM customers WHERE city = 'Göteborg';
  3. SELECT name FROM customers ORDER BY name ASC LIMIT 10;
  4. INSERT INTO customers (name, email, city) VALUES ('Lisa Berg', 'lisa@example.com', 'Malmö');
  5. UPDATE customers SET city = 'Lund' WHERE id = 15;
  6. DELETE FROM customers WHERE id = 20;
  7. SELECT orders.order_id, customers.name FROM orders INNER JOIN customers ON orders.customer_id = customers.id;

Sessioner och Säkerhet (Koncept/PHP)

  1. Funktionen är session_start(). Den bör placeras allra högst upp i PHP-skriptet, före all annan output (HTML, mellanslag, echo etc.).
  2. <?php
    session_start();
    $_SESSION['theme'] = 'dark';
    ?>
    
  3. <?php
    session_start();
    if (isset($_SESSION['user_id'])) {
        echo "Användaren är inloggad.";
    } else {
        echo "Användaren är inte inloggad.";
    }
    ?>
    
  4. Funktionen är htmlspecialchars(). Exempel:
    <?php 
    $userInput = "<script>alert('hack');</script>"; 
    echo htmlspecialchars($userInput, ENT_QUOTES, 'UTF-8'); 
    // Output blir ofarlig text: &lt;script&gt;alert(&#039;hack&#039;);&lt;/script&gt;
    ?>
    
  5. Tekniken är Prepared Statements. I PDO initieras detta med $pdo->prepare("SQL med :placeholders...").
  6. password_hash(): Skapar en stark, saltad hash av ett lösenord. password_verify(): Jämför ett inskickat lösenord mot en lagrad hash på ett säkert sätt.
  7. Anti-CSRF Tokens (Synchronizer Token Pattern): Generera en unik token, lagra den i sessionen, inkludera den i formuläret, och verifiera att den inskickade token matchar sessionens token på serversidan innan åtgärden utförs.

Mini-CRUD (PHP/PDO)

  1. <?php
    // Antag att formuläret skickats via POST
    $description = trim($_POST['task_description'] ?? '');
    // Ytterligare validering kan behövas (t.ex. kolla om tom)
    ?>
    
  2. <?php
    // Antag att $pdo är en giltig PDO-anslutning och $description har ett värde
    try {
        $sql = "INSERT INTO tasks (description) VALUES (:desc)";
        $stmt = $pdo->prepare($sql);
        $stmt->bindParam(':desc', $description, PDO::PARAM_STR);
        $stmt->execute();
        echo "Uppgift tillagd!";
    } catch (PDOException $e) {
        error_log("Insert Task Error: " . $e->getMessage());
        echo "Kunde inte lägga till uppgift.";
    }
    ?>
    
  3. <?php
    // Antag att $pdo är en giltig PDO-anslutning
    $allTasks = [];
    try {
        $sql = "SELECT id, description FROM tasks ORDER BY id";
        $stmt = $pdo->query($sql); // Enkel query går bra här då ingen input finns
        $allTasks = $stmt->fetchAll(); 
    } catch (PDOException $e) {
        error_log("Fetch Tasks Error: " . $e->getMessage());
        echo "Kunde inte hämta uppgifter.";
    }
    // Nu innehåller $allTasks en array med alla uppgifter (eller är tom om fel)
    // print_r($allTasks);
    ?>
    

Klasser (OOP Grunderna)

  1. <?php
    declare(strict_types=1);
    
    class Book 
    {
        public string $title;
        public string $author;
    
        public function displayInfo(): void 
        {
            echo "Title: " . $this->title . ", Author: " . $this->author . "\n";
        }
    }
    ?>
    
  2. <?php
    // Antag att Book-klassen från övning 35 finns
    
    $book1 = new Book();
    $book1->title = "Ronja Rövardotter";
    $book1->author = "Astrid Lindgren";
    
    $book2 = new Book();
    $book2->title = "Sagan om Ringen";
    $book2->author = "J.R.R. Tolkien";
    
    $book1->displayInfo(); // Output: Title: Ronja Rövardotter, Author: Astrid Lindgren
    $book2->displayInfo(); // Output: Title: Sagan om Ringen, Author: J.R.R. Tolkien
    ?>
    
  3. <?php
    declare(strict_types=1);
    
    class Book 
    {
        public string $title;
        public string $author;
    
        // Konstruktor
        public function __construct(string $bookTitle, string $bookAuthor)
        {
            $this->title = $bookTitle;
            $this->author = $bookAuthor;
        }
    
        public function displayInfo(): void 
        {
            echo "Title: " . $this->title . ", Author: " . $this->author . "\n";
        }
    }
    
    // Skapa objekt med konstruktor
    $book3 = new Book("Liftarens Guide till Galaxen", "Douglas Adams");
    $book3->displayInfo(); // Output: Title: Liftarens Guide till Galaxen, Author: Douglas Adams
    ?>
    
  4. <?php
    declare(strict_types=1);
    
    class Book 
    {
        private string $title;
        private string $author;
        public int $pages; // Lämnar denna publik som exempel
    
        public function __construct(string $bookTitle, string $bookAuthor, int $numPages)
        {
            $this->title = $bookTitle;
            $this->author = $bookAuthor;
            $this->pages = $numPages; // Sätter den publika egenskapen
        }
    
        // Getter för titel
        public function getTitle(): string 
        {
            return $this->title;
        }
    
        // Getter för författare
        public function getAuthor(): string 
        {
            return $this->author;
        }
    
        // Getter för sidantal (tekniskt sett inte nödvändig då pages är publik, men bra för konsekvens)
        public function getPages(): int 
        {
            return $this->pages;
        }
    
        // Vi kan behålla displayInfo om vi vill, men den behöver använda getters nu
        public function displayInfo(): void 
        {
            echo "Title: " . $this->getTitle() . ", Author: " . $this->getAuthor() . ", Pages: " . $this->getPages() . "\n";
        }
    }
    ?>
    
  5. <?php
    // Antag att den modifierade Book-klassen från övning 38 finns
    
    $book4 = new Book("Mio min Mio", "Astrid Lindgren", 180);
    
    // Hämta värden med getters
    $title = $book4->getTitle();
    $author = $book4->getAuthor();
    $pages = $book4->getPages(); // Kan också nås direkt: $book4->pages
    
    echo "Bok: " . $title . "\n";
    echo "Författare: " . $author . "\n";
    echo "Antal sidor: " . $pages . "\n";
    
    // Anropa displayInfo som nu använder getters internt
    $book4->displayInfo();
    ?>
    

Teknisk Intervju: Fullstack-utveckling med PHP

Detta avsnitt innehåller vanliga tekniska intervjufrågor för fullstack-utveckling med PHP och MySQL. Frågorna täcker grundläggande PHP-syntax, objektorienterad programmering, databasinteraktion, säkerhet och CRUD-operationer.

Fråga 1: PHP Grundläggande Syntax och Variabler

Intervjuare: "Kan du förklara skillnaden mellan PHP och JavaScript när det gäller var koden exekveras? Visa också hur man deklarerar och använder variabler i PHP."

Bra svar:

<?php
// PHP körs på SERVERN (server-side)
// JavaScript körs i WEBBLÄSAREN (client-side)

// PHP-variabler börjar alltid med $
$productName = "Laptop";
$productPrice = 999.50;

// Strängkonkatenering med punkt (.)
echo "Produkten " . $productName . " kostar " . $productPrice . " kr.";

// Alternativt med interpolering i dubbla citationstecken
echo "Produkten $productName kostar $productPrice kr.";
?>

Förklaring: PHP exekveras på servern innan HTML skickas till webbläsaren, medan JavaScript körs i användarens webbläsare. PHP-variabler börjar med $ och använder . för strängkonkatenering.

Intervjutips: Betona skillnaden mellan server-side och client-side exekvering - detta är fundamentalt för fullstack-utveckling.

Fråga 2: PHP Arrays och Loopar

Intervjuare: "Vad är skillnaden mellan indexerade och associativa arrays i PHP? Visa hur man använder foreach för båda typerna."

Bra svar:

<?php
// Indexerad array (numeriska index)
$cities = ["Stockholm", "Göteborg", "Malmö"];

// Associativ array (namngivna nycklar)
$person = [
    "firstName" => "Anna",
    "lastName" => "Andersson",
    "age" => 30
];

// Foreach för indexerad array (endast värden)
foreach ($cities as $city) {
    echo $city . "<br>";
}

// Foreach för associativ array (nyckel och värde)
foreach ($person as $key => $value) {
    echo $key . ": " . $value . "<br>";
}

// Kontrollera om värde finns
if (in_array("Stockholm", $cities)) {
    echo "Stockholm finns i listan";
}

// Kontrollera om nyckel finns
if (isset($person["age"])) {
    echo "Ålder: " . $person["age"];
}
?>

Förklaring: Indexerade arrays använder numeriska index (0, 1, 2...), medan associativa arrays använder namngivna strängnycklar. Foreach är den mest idiomatiska loopen för arrays i PHP.

Fråga 3: PHP Funktioner och Type Hinting

Intervjuare: "Skriv en funktion som beräknar rabatt på ett pris. Använd type hinting och förklara fördelarna med strict types."

Bra svar:

<?php
declare(strict_types=1); // Aktiverar strikt typkontroll

function calculateDiscount(float $price, int $percentage): float {
    if ($percentage < 0 || $percentage > 100) {
        throw new InvalidArgumentException('Procentsats måste vara mellan 0-100');
    }
    
    $discountFactor = 1 - ($percentage / 100.0);
    return $price * $discountFactor;
}

// Användning
try {
    $originalPrice = 1000.0;
    $discountedPrice = calculateDiscount($originalPrice, 20);
    echo "Pris efter 20% rabatt: " . $discountedPrice . " kr";
} catch (InvalidArgumentException $e) {
    echo "Fel: " . $e->getMessage();
}
?>

Förklaring: Type hinting specificerar förväntade datatyper för parametrar och returvärden. declare(strict_types=1) förhindrar automatisk typkonvertering och fångar fel tidigare.

Fråga 4: Objektorienterad Programmering i PHP

Intervjuare: "Skapa en enkel PHP-klass för en produkt med konstruktor, privata properties och getter/setter-metoder. Förklara fördelarna med inkapsling."

Bra svar:

<?php
declare(strict_types=1);

class Product {
    private string $name;
    private float $price;
    private int $stock;

    public function __construct(string $name, float $price, int $stock = 0) {
        $this->name = $name;
        $this->setPrice($price); // Använd setter för validering
        $this->setStock($stock);
    }

    // Getters
    public function getName(): string {
        return $this->name;
    }

    public function getPrice(): float {
        return $this->price;
    }

    public function getStock(): int {
        return $this->stock;
    }

    // Setters med validering
    public function setPrice(float $price): void {
        if ($price < 0) {
            throw new InvalidArgumentException('Priset kan inte vara negativt');
        }
        $this->price = $price;
    }

    public function setStock(int $stock): void {
        if ($stock < 0) {
            throw new InvalidArgumentException('Lagersaldo kan inte vara negativt');
        }
        $this->stock = $stock;
    }

    public function isInStock(): bool {
        return $this->stock > 0;
    }
}

// Användning
$product = new Product("Laptop", 15999.0, 5);
echo $product->getName() . " kostar " . $product->getPrice() . " kr";
?>

Förklaring: Inkapsling (private properties + public methods) ger kontroll över hur data nås och modifieras, möjliggör validering och förhindrar direktmanipulation av objektets tillstånd.

Fråga 5: SQL och Databasinteraktion

Intervjuare: "Skriv SQL-frågor för att skapa en users-tabell och visa hur man säkert hämtar användardata med PHP PDO."

Bra svar:

-- Skapa users-tabell
CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) NOT NULL UNIQUE,
    email VARCHAR(100) NOT NULL UNIQUE,
    password_hash VARCHAR(255) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Hämta användare från specifik stad
SELECT id, username, email FROM users 
WHERE city = 'Stockholm' 
ORDER BY username ASC;
<?php
// PHP PDO för säker databasinteraktion
function getUserByUsername(PDO $pdo, string $username): array|false {
    // Prepared statement förhindrar SQL injection
    $stmt = $pdo->prepare("SELECT id, username, email, created_at 
                           FROM users 
                           WHERE username = :username");
    $stmt->bindParam(':username', $username, PDO::PARAM_STR);
    $stmt->execute();
    
    return $stmt->fetch(PDO::FETCH_ASSOC);
}

// Databasanslutning
try {
    $pdo = new PDO(
        'mysql:host=localhost;dbname=myapp;charset=utf8mb4',
        $username,
        $password,
        [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
    );
    
    $user = getUserByUsername($pdo, 'kalleanka');
    if ($user) {
        echo "Användare: " . htmlspecialchars($user['username']);
    }
} catch (PDOException $e) {
    error_log("Database error: " . $e->getMessage());
    echo "Databasfel uppstod";
}
?>

Förklaring: Prepared statements är avgörande för att förhindra SQL injection. PDO ger ett konsekvent interface för olika databaser.

Fråga 6: PHP Sessioner och Säkerhet

Intervjuare: "Förklara hur PHP-sessioner fungerar och visa hur man implementerar en säker inloggningsprocess."

Bra svar:

<?php
// Starta session (alltid högst upp)
session_start();

function loginUser(PDO $pdo, string $username, string $password): bool {
    // Hämta användare från databas
    $stmt = $pdo->prepare("SELECT id, username, password_hash 
                           FROM users 
                           WHERE username = :username");
    $stmt->bindParam(':username', $username);
    $stmt->execute();
    $user = $stmt->fetch();

    // Verifiera lösenord
    if ($user && password_verify($password, $user['password_hash'])) {
        // Regenerera session-ID för säkerhet
        session_regenerate_id(true);
        
        // Spara användarinfo i session
        $_SESSION['user_id'] = $user['id'];
        $_SESSION['username'] = $user['username'];
        
        return true;
    }
    
    return false;
}

function isLoggedIn(): bool {
    return isset($_SESSION['user_id']);
}

function requireLogin(): void {
    if (!isLoggedIn()) {
        header('Location: login.php');
        exit;
    }
}

function logout(): void {
    $_SESSION = [];
    
    // Ta bort session cookie
    if (ini_get("session.use_cookies")) {
        $params = session_get_cookie_params();
        setcookie(session_name(), '', time() - 42000,
            $params["path"], $params["domain"],
            $params["secure"], $params["httponly"]
        );
    }
    
    session_destroy();
}
?>

Förklaring: Sessioner lagrar data på servern och använder cookies för att identifiera användare. password_verify() och session_regenerate_id() är viktiga säkerhetsåtgärder.

Fråga 7: CRUD Operations med PHP

Intervjuare: "Implementera grundläggande CRUD-operationer för en bloggpost med PHP och MySQL."

Bra svar:

<?php
class PostRepository {
    private PDO $pdo;

    public function __construct(PDO $pdo) {
        $this->pdo = $pdo;
    }

    // CREATE
    public function createPost(string $title, string $content, int $userId): int {
        $stmt = $this->pdo->prepare(
            "INSERT INTO posts (title, content, user_id, created_at) 
             VALUES (:title, :content, :user_id, NOW())"
        );
        
        $stmt->bindParam(':title', $title);
        $stmt->bindParam(':content', $content);
        $stmt->bindParam(':user_id', $userId, PDO::PARAM_INT);
        $stmt->execute();
        
        return (int)$this->pdo->lastInsertId();
    }

    // READ
    public function getPostById(int $id): array|false {
        $stmt = $this->pdo->prepare(
            "SELECT p.*, u.username 
             FROM posts p 
             JOIN users u ON p.user_id = u.id 
             WHERE p.id = :id"
        );
        $stmt->bindParam(':id', $id, PDO::PARAM_INT);
        $stmt->execute();
        
        return $stmt->fetch(PDO::FETCH_ASSOC);
    }

    // UPDATE
    public function updatePost(int $id, string $title, string $content, int $userId): bool {
        $stmt = $this->pdo->prepare(
            "UPDATE posts 
             SET title = :title, content = :content, updated_at = NOW()
             WHERE id = :id AND user_id = :user_id"
        );
        
        $stmt->bindParam(':title', $title);
        $stmt->bindParam(':content', $content);
        $stmt->bindParam(':id', $id, PDO::PARAM_INT);
        $stmt->bindParam(':user_id', $userId, PDO::PARAM_INT);
        
        return $stmt->execute() && $stmt->rowCount() > 0;
    }

    // DELETE
    public function deletePost(int $id, int $userId): bool {
        $stmt = $this->pdo->prepare(
            "DELETE FROM posts 
             WHERE id = :id AND user_id = :user_id"
        );
        
        $stmt->bindParam(':id', $id, PDO::PARAM_INT);
        $stmt->bindParam(':user_id', $userId, PDO::PARAM_INT);
        
        return $stmt->execute() && $stmt->rowCount() > 0;
    }
}
?>

Förklaring: CRUD-operationer (Create, Read, Update, Delete) är grundläggande för datahantering. Inkludera användar-ID i UPDATE/DELETE för säkerhet.

Fråga 8: Formulärhantering och Validering

Intervjuare: "Visa hur man säkert hanterar formulärdata i PHP med validering och felhantering."

Bra svar:

<?php
function handleContactForm(): array {
    $errors = [];
    $data = [];

    if ($_SERVER['REQUEST_METHOD'] === 'POST') {
        // Hämta och sanera input
        $data['name'] = trim($_POST['name'] ?? '');
        $data['email'] = trim($_POST['email'] ?? '');
        $data['message'] = trim($_POST['message'] ?? '');

        // Validering
        if (empty($data['name'])) {
            $errors[] = 'Namn är obligatoriskt';
        } elseif (strlen($data['name']) < 2) {
            $errors[] = 'Namnet måste vara minst 2 tecken';
        }

        if (empty($data['email'])) {
            $errors[] = 'E-post är obligatoriskt';
        } elseif (!filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
            $errors[] = 'Ogiltig e-postadress';
        }

        if (empty($data['message'])) {
            $errors[] = 'Meddelande är obligatoriskt';
        } elseif (strlen($data['message']) < 10) {
            $errors[] = 'Meddelandet måste vara minst 10 tecken';
        }

        // Om inga fel, behandla formuläret
        if (empty($errors)) {
            // Spara till databas, skicka e-post etc.
            // Omdirigera för att förhindra dubbel inlämning
            header('Location: success.php');
            exit;
        }
    }

    return ['errors' => $errors, 'data' => $data];
}

// I HTML-templaten
$result = handleContactForm();
?>

<form method="post">
    <?php if (!empty($result['errors'])): ?>
        <div class="errors">
            <?php foreach ($result['errors'] as $error): ?>
                <p><?= htmlspecialchars($error) ?></p>
            <?php endforeach; ?>
        </div>
    <?php endif; ?>
    
    <input type="text" name="name" 
           value="<?= htmlspecialchars($result['data']['name'] ?? '') ?>">
    <input type="email" name="email" 
           value="<?= htmlspecialchars($result['data']['email'] ?? '') ?>">
    <textarea name="message"><?= htmlspecialchars($result['data']['message'] ?? '') ?></textarea>
    <button type="submit">Skicka</button>
</form>

Förklaring: Validera alltid användarinput på serversidan. Använd htmlspecialchars() för att förhindra XSS när data skrivs ut.

Fråga 9: Filuppladdning och Säkerhet

Intervjuare: "Implementera säker filuppladdning för bilder i PHP. Vilka säkerhetsrisker finns?"

Bra svar:

<?php
function handleImageUpload(array $file): array {
    $errors = [];
    $uploadPath = null;

    // Kontrollera att fil laddades upp utan fel
    if ($file['error'] !== UPLOAD_ERR_OK) {
        $errors[] = 'Filuppladdning misslyckades';
        return ['errors' => $errors, 'path' => null];
    }

    // Validera filstorlek (max 5MB)
    $maxSize = 5 * 1024 * 1024;
    if ($file['size'] > $maxSize) {
        $errors[] = 'Filen är för stor (max 5MB)';
    }

    // Validera filtyp (kontrollera MIME type OCH filändelse)
    $allowedMimes = ['image/jpeg', 'image/png', 'image/gif'];
    $allowedExts = ['jpg', 'jpeg', 'png', 'gif'];
    
    $finfo = finfo_open(FILEINFO_MIME_TYPE);
    $mimeType = finfo_file($finfo, $file['tmp_name']);
    finfo_close($finfo);
    
    $extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));

    if (!in_array($mimeType, $allowedMimes) || !in_array($extension, $allowedExts)) {
        $errors[] = 'Endast JPG, PNG och GIF-filer tillåtna';
    }

    if (empty($errors)) {
        // Skapa säkert filnamn
        $safeName = uniqid('img_', true) . '.' . $extension;
        $uploadDir = __DIR__ . '/uploads/';
        $targetPath = $uploadDir . $safeName;

        // Skapa directory om det inte finns
        if (!is_dir($uploadDir)) {
            mkdir($uploadDir, 0755, true);
        }

        // Flytta fil till slutgiltig plats
        if (move_uploaded_file($file['tmp_name'], $targetPath)) {
            $uploadPath = 'uploads/' . $safeName;
        } else {
            $errors[] = 'Kunde inte spara filen';
        }
    }

    return ['errors' => $errors, 'path' => $uploadPath];
}

// Användning
if (isset($_FILES['image'])) {
    $result = handleImageUpload($_FILES['image']);
    if (empty($result['errors'])) {
        echo "Fil uppladdad: " . htmlspecialchars($result['path']);
    }
}
?>

Säkerhetsrisker:

  • Executable uploads: Aldrig tillåt .php, .exe filer
  • Directory traversal: Använd säkra filnamn
  • MIME type spoofing: Kontrollera både MIME och filändelse
  • Filstorlek: Begränsa för att förhindra DoS

Fråga 10: XSS och CSRF-skydd

Intervjuare: "Förklara XSS och CSRF-attacker. Hur skyddar man sig i PHP?"

Bra svar:

XSS (Cross-Site Scripting) Skydd:

<?php
// ALLTID sanera output med htmlspecialchars()
function safeOutput(string $data): string {
    return htmlspecialchars($data, ENT_QUOTES, 'UTF-8');
}

// Farlig kod (sårbar för XSS)
echo $_GET['message']; // ❌ Farligt!

// Säker kod
echo safeOutput($_GET['message']); // ✅ Säkert

// För HTML-innehåll, använd whitelist-baserad rensning
// t.ex. HTMLPurifier biblioteket
?>

CSRF (Cross-Site Request Forgery) Skydd:

<?php
session_start();

function generateCSRFToken(): string {
    if (!isset($_SESSION['csrf_token'])) {
        $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
    }
    return $_SESSION['csrf_token'];
}

function validateCSRFToken(string $token): bool {
    return isset($_SESSION['csrf_token']) && 
           hash_equals($_SESSION['csrf_token'], $token);
}

// I formulär
$csrfToken = generateCSRFToken();
?>

<form method="post">
    <input type="hidden" name="csrf_token" value="<?= $csrfToken ?>">
    <!-- Övriga formulärfält -->
    <button type="submit">Skicka</button>
</form>

<?php
// Vid formulärhantering
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    if (!validateCSRFToken($_POST['csrf_token'] ?? '')) {
        die('CSRF-token validation failed');
    }
    
    // Fortsätt med formulärbehandling...
}
?>

Förklaring: XSS förhindras genom att sanera all output. CSRF förhindras med unika tokens som valideras vid formulärinlämning.

Fråga 11: Error Handling och Debugging

Intervjuare: "Visa hur man implementerar robust felhantering i en PHP-applikation."

Bra svar:

<?php
// Aktivera strict types och error reporting för utveckling
declare(strict_types=1);
ini_set('display_errors', 1);
error_reporting(E_ALL);

// Custom exception klasser
class ValidationException extends Exception {}
class DatabaseException extends Exception {}

class UserService {
    private PDO $pdo;
    private LoggerInterface $logger;

    public function createUser(array $userData): int {
        try {
            // Validering
            $this->validateUserData($userData);
            
            // Databasoperation
            $stmt = $this->pdo->prepare(
                "INSERT INTO users (username, email, password_hash) 
                 VALUES (:username, :email, :password)"
            );
            
            $passwordHash = password_hash($userData['password'], PASSWORD_DEFAULT);
            
            $stmt->execute([
                ':username' => $userData['username'],
                ':email' => $userData['email'],
                ':password' => $passwordHash
            ]);
            
            $userId = (int)$this->pdo->lastInsertId();
            $this->logger->info("User created", ['user_id' => $userId]);
            
            return $userId;
            
        } catch (ValidationException $e) {
            // Logga validering fel (för utveckling)
            $this->logger->warning("Validation failed", [
                'error' => $e->getMessage(),
                'data' => $userData
            ]);
            throw $e; // Re-throw för hantering i controller
            
        } catch (PDOException $e) {
            // Logga databasfel (känslig info)
            $this->logger->error("Database error creating user", [
                'error' => $e->getMessage(),
                'code' => $e->getCode()
            ]);
            
            // Kasta generellt fel till användaren
            throw new DatabaseException('Could not create user');
            
        } catch (Exception $e) {
            // Fånga oväntade fel
            $this->logger->critical("Unexpected error", [
                'error' => $e->getMessage(),
                'trace' => $e->getTraceAsString()
            ]);
            
            throw new Exception('An unexpected error occurred');
        }
    }

    private function validateUserData(array $data): void {
        if (empty($data['username'])) {
            throw new ValidationException('Username is required');
        }
        
        if (!filter_var($data['email'], FILTER_VALIDATE_EMAIL)) {
            throw new ValidationException('Invalid email format');
        }
        
        if (strlen($data['password']) < 8) {
            throw new ValidationException('Password must be at least 8 characters');
        }
    }
}

// Global exception handler för produktion
set_exception_handler(function(Throwable $e) {
    error_log("Uncaught exception: " . $e->getMessage());
    
    // Visa användarvänligt meddelande
    http_response_code(500);
    echo "Something went wrong. Please try again later.";
});
?>

Förklaring: Använd specifika exceptions, logga tekniska fel separat från användarmeddelanden, och ha en global exception handler som backup.

Fråga 12: Performance och Optimering

Intervjuare: "Vilka tekniker kan du använda för att optimera prestanda i en PHP-applikation?"

Bra svar:

1. Databas-optimering:

<?php
// Använd index på ofta söka kolumner
// CREATE INDEX idx_username ON users(username);
// CREATE INDEX idx_email ON users(email);

// Begränsa SELECT-kolumner
$stmt = $pdo->prepare("SELECT id, username FROM users WHERE active = 1");
// Istället för: SELECT * FROM users WHERE active = 1

// Använd LIMIT för paginering
$stmt = $pdo->prepare("SELECT * FROM posts ORDER BY created_at DESC LIMIT :offset, :limit");
$stmt->bindValue(':offset', ($page - 1) * $perPage, PDO::PARAM_INT);
$stmt->bindValue(':limit', $perPage, PDO::PARAM_INT);
?>

2. Caching:

<?php
// APCu cache för smådata
function getCachedUserCount(): int {
    $key = 'user_count';
    $count = apcu_fetch($key);
    
    if ($count === false) {
        // Hämta från databas
        $stmt = $pdo->query("SELECT COUNT(*) FROM users");
        $count = (int)$stmt->fetchColumn();
        
        // Cache i 5 minuter
        apcu_store($key, $count, 300);
    }
    
    return $count;
}

// Filbaserad cache för komplexare data
function getCachedPosts(): array {
    $cacheFile = '/tmp/posts_cache.json';
    $cacheTime = 600; // 10 minuter
    
    if (file_exists($cacheFile) && (time() - filemtime($cacheFile)) < $cacheTime) {
        return json_decode(file_get_contents($cacheFile), true);
    }
    
    // Hämta från databas
    $posts = fetchPostsFromDatabase();
    file_put_contents($cacheFile, json_encode($posts));
    
    return $posts;
}
?>

3. OPcache och autoloading:

<?php
// php.ini inställningar för OPcache
/*
opcache.enable=1
opcache.memory_consumption=128
opcache.max_accelerated_files=4000
opcache.revalidate_freq=60
*/

// Composer autoloader för effektiv class loading
require_once 'vendor/autoload.php';

// Lazy loading av tunga objekt
class Application {
    private ?DatabaseConnection $db = null;
    
    public function getDatabase(): DatabaseConnection {
        if ($this->db === null) {
            $this->db = new DatabaseConnection();
        }
        return $this->db;
    }
}
?>

4. Memory och I/O optimering:

<?php
// Undvik minneskrävande operationer
// Istället för att ladda alla rader:
// $allUsers = $stmt->fetchAll(); // ❌ Kan använda mycket minne

// Använd generators för stora dataset:
function getUsersGenerator(PDO $pdo): Generator {
    $stmt = $pdo->query("SELECT * FROM users");
    while ($row = $stmt->fetch()) {
        yield $row;
    }
}

// Buffra output för bättre prestanda
ob_start();
foreach (getUsersGenerator($pdo) as $user) {
    echo "<div>" . htmlspecialchars($user['username']) . "</div>";
}
ob_end_flush();
?>

Prestanda-tips:

  • Använd OPcache för att cacha kompilerad PHP-kod
  • Implementera databas-indexering på ofta använda kolumner
  • Cacha dyra operationer (API-anrop, komplexa beräkningar)
  • Använd generators för stora dataset
  • Minimera databas-queries med eager loading
  • Komprimera CSS/JS och använd CDN för statiska tillgångar

Intervjutips: Diskutera olika nivåer av optimering - från kod-nivå till infrastruktur (load balancers, database replicas etc.).


Sammanfattning

Dessa frågor täcker de viktigaste aspekterna av fullstack-utveckling med PHP:

  • Grundläggande PHP: Syntax, variabler, funktioner
  • Datastrukturer: Arrays, objekt, klasser
  • Databaser: SQL, PDO, prepared statements
  • Säkerhet: XSS, CSRF, lösenordshantering, filuppladdning
  • Arkitektur: CRUD, error handling, prestanda

Förberedelse-tips:

  1. Öva på att skriva PHP-kod utan IDE-hjälp
  2. Memorera vanliga säkerhetsprinciper och best practices
  3. Förstå skillnaden mellan server-side och client-side kod
  4. Repetera SQL-grunderna och JOIN-operationer
  5. Var beredd att diskutera prestanda-optimeringar

Kapitel 8: Frontend-ramverk med React

I de tidigare kapitlen har vi byggt grunderna för webbutveckling med HTML, CSS, JavaScript och PHP. Vi har lärt oss skapa dynamiska webbapplikationer med server-side rendering och databaser. Nu är det dags att ta steget in i modern frontend-utveckling med React.

Detta kapitel introducerar dig till komponentsbaserad utveckling och Single Page Applications (SPA) - en arkitektur som dominerar moderna webbapplikationer. Istället för att generera HTML på servern och skicka hela sidor, bygger vi interaktiva användargränssnitt som körs i webbläsaren och kommunicerar med servern via API:er.

Vad är React? React är ett JavaScript-bibliotek utvecklat av Facebook (Meta) för att bygga användargränssnitt. Det fokuserar på att skapa återanvändbara komponenter och hanterar effektivt uppdateringar av DOM:en genom sitt Virtual DOM-system.

Varför React?

  1. Komponentbaserad arkitektur: Bygg applikationer som LEGO-block där varje komponent har sitt eget ansvar
  2. Återanvändbarhet: Skriv en gång, använd överallt
  3. Prestanda: Virtual DOM optimerar uppdateringar automatiskt
  4. Stort ekosystem: Omfattande community och bibliotek
  5. Industristandard: Används av företag som Facebook, Netflix, Airbnb, Uber

Single Page Applications (SPA) skiljer sig från traditionella webbapplikationer genom att:

  • Ladda appen en gång, sedan uppdatera innehållet dynamiskt
  • Navigering sker utan att hela sidan laddas om
  • Snabbare användarupplevelse efter initial laddning
  • Tydlig separation mellan frontend och backend

Vad är en Komponentbaserad arkitektur?

En komponent är en JavaScript-funktion eller klass som returnerar JSX och representerar en del av användargränssnittet. Tänk på det som en anpassad HTML-tagg som du själv definierar.

graph TD
    A[App] --> B[Header]
    A --> C[Main]
    A --> D[Footer]
    
    B --> E[Logo]
    B --> F[Navigation]
    
    C --> G[ProductList]
    C --> H[Sidebar]
    
    G --> I[ProductCard]
    G --> J[ProductCard]
    G --> K[ProductCard]
    
    H --> L[SearchFilter]
    H --> M[CategoryFilter]

    style A fill:#4CAF50
    style G fill:#FF5722
    style I fill:#FF5722 
    style J fill:#FF5722
    style K fill:#FF5722

Diagram: Komponenthierarki för en e-handelsapplikation

I detta kapitel kommer vi att:

  • Din första komponent: Lära dig skapa och organisera React-komponenter
  • Skriva markup med JSX: Kombinera JavaScript och HTML-liknande syntax
  • Props och interaktivitet: Skicka data mellan komponenter och hantera events
  • State: Ge komponenter minne för att reagera på förändringar
  • Formulär: Hantera användarinput på ett kontrollerat sätt
  • API-integration: Hämta och skicka data till backend-tjänster
  • Routing: Bygga Single Page Applications med navigation
  • Deployment: Publicera din app på internet

Förutsättningar: Du bör ha god kunskap i JavaScript (ES6+), HTML, CSS och grundläggande förståelse för API:er från tidigare kapitel.

Program som ska vara installerade

Innan vi börjar behöver du ha följande verktyg installerade:

NVM (Node Version Manager)

NVM används för att hantera olika versioner av Node.js. Det låter dig enkelt byta mellan olika Node.js-versioner.

Installation:

  • Windows: Ladda ner och installera nvm-windows
  • macOS/Linux: Kör följande kommando i terminalen:
    curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
    

Efter installation, kör följande kommandon för att installera och använda senaste LTS-versionen av Node.js:

nvm install --lts # installera senaste long-term-support versionen
nvm use --lts # använd den

Verifiera installationen genom att köra:

node --version # v22.18.0 eller högre

Låt oss börja vår resa in i modern frontend-utveckling!

Din Första React-komponent

React är ett JavaScript-bibliotek för att bygga användargränssnitt. UI byggs från små enheter som knappar, text och bilder. React låter dig kombinera dem till återanvändbara, nästlade komponenter. Från webbplatser till mobilappar - allt på skärmen kan delas upp i komponenter.

Mål: Skapa din första React-komponent, förstå JSX-syntax och sätta upp en utvecklingsmiljö.

Vad är en Komponent?

En komponent är en JavaScript-funktion som returnerar markup (JSX). Komponenter kan vara så små som en knapp, eller så stora som en hel sida.

// Din första komponent - en enkel funktion som returnerar JSX
function Välkomstmeddelande() {
  return <h1>Hej från React!</h1>;
}

// Använd komponenten som en HTML-tagg
function App() {
  return (
    <div>
      <Välkomstmeddelande />
      <Välkomstmeddelande />
      <Välkomstmeddelande />
    </div>
  );
}

Prova detta! Skapa en ny React-app och ersätt innehållet i App.js med koden ovan.

Komponenter Överallt

React-applikationer byggs från isolerade UI-delar som kallas komponenter. Här är en Galleri-komponent som renderar tre Profil-komponenter:

function Profil() {
  return (
    <img
      src="https://i.imgur.com/MK3eW3As.jpg"
      alt="Katherine Johnson"
    />
  );
}

function Galleri() {
  return (
    <section>
      <h1>Fantastiska forskare</h1>
      <Profil />
      <Profil />
      <Profil />
    </section>
  );
}

Importera och Exportera Komponenter

Du kan deklarera många komponenter i en fil, men stora filer kan bli svåra att navigera. För att lösa detta kan du exportera en komponent till sin egen fil och sedan importera den komponenten från en annan fil:

// Profil.js
function Profil() {
  return (
    <img
      src="https://i.imgur.com/MK3eW3As.jpg"
      alt="Katherine Johnson"
    />
  );
}

export default Profil;
// Galleri.js
import Profil from './Profil.js';

function Galleri() {
  return (
    <section>
      <h1>Fantastiska forskare</h1>
      <Profil />
      <Profil />
      <Profil />
    </section>
  );
}

export default Galleri;

Ditt UI som ett Träd

React använder träd för att modellera relationerna mellan komponenter och moduler.

En React render-träd är en representation av förälder- och barnrelationen mellan komponenter.

graph TB
    A["Root Component"] --> B["Component A"]
    A --> C["Component C"] 
    B --> D["Component B"]
    C --> E["Component D"]
    
    style A fill:#e8f5e8,stroke:#388e3c,stroke-width:2px,color:#000
    style B fill:#e3f2fd,stroke:#1976d2,stroke-width:2px,color:#000
    style C fill:#e3f2fd,stroke:#1976d2,stroke-width:2px,color:#000
    style D fill:#fff3e0,stroke:#f57c00,stroke-width:2px,color:#000
    style E fill:#fff3e0,stroke:#f57c00,stroke-width:2px,color:#000

Komponenter nära toppen av trädet, nära root-komponenten, betraktas som toppnivå-komponenter. Komponenter utan barnkomponenter är löv-komponenter. Denna kategorisering av komponenter är användbar för att förstå dataflöde och renderingsprestanda.

Virtual DOM: Prestanda Under Huven (Fördjupning)

Nu när du förstår grunderna, låt oss titta på Virtual DOM - ett av Reacts mest innovativa koncept.

Virtual DOM-processen:

  1. Skapande: React skapar en virtuell representation av DOM:en i JavaScript
  2. Jämförelse (Diffing): När något ändras jämför React den nya Virtual DOM med den föregående
  3. Minimal uppdatering: Bara de delar som faktiskt ändrats uppdateras i den riktiga DOM:en

Virtual DOM:s verkliga fördelar:

  • Batching: Flera state-ändringar → en DOM-uppdatering
  • Smart diffing: Hoppar över onödiga uppdateringar
  • Förutsägbarhet: Deklarativ kod istället för imperativ DOM-manipulation

Virtual DOM vs Real DOM: Träd-struktur

För att förstå Virtual DOM bättre, låt oss visualisera hur React hanterar ändringar i en HTML-struktur:

graph TB
    subgraph realdom ["Real DOM (Långsam)"]
        direction TB
        RD1["div#app"] --> RD2["header"]
        RD1 --> RD3["main"]
        RD1 --> RD4["footer"]
        RD2 --> RD5["h1: 'Välkommen'"]
        RD3 --> RD6["div.content"]
        RD3 --> RD7["button: 'Klicka'"]
        RD6 --> RD8["p: 'Räknare: 0'"]
        
        RD8_NEW["p: 'Räknare: 1' ⚡"]
        RD8 -.->|"Flera separata<br/>DOM-operationer"| RD8_NEW
    end
    
    subgraph vdom ["Virtual DOM (Snabb)"]
        direction TB
        VD1["div#app"] --> VD2["header"]
        VD1 --> VD3["main"]
        VD1 --> VD4["footer"]
        VD2 --> VD5["h1: 'Välkommen'"]
        VD3 --> VD6["div.content"]
        VD3 --> VD7["button: 'Klicka'"]
        VD6 --> VD8["p: 'Räknare: 0'"]
        
        VD8_NEW["p: 'Räknare: 1' ⚡"]
        VD8 -.->|"Bara denna nod<br/>uppdateras"| VD8_NEW
    end
    
    style realdom fill:#ffebee,stroke:#d32f2f,stroke-width:2px,color:#000
    style vdom fill:#e8f5e8,stroke:#388e3c,stroke-width:2px,color:#000
    
    style RD8 fill:#ffcdd2,stroke:#d32f2f,color:#000
    style RD8_NEW fill:#ef5350,stroke:#d32f2f,color:#fff
    style VD8 fill:#c8e6c9,stroke:#388e3c,color:#000
    style VD8_NEW fill:#66bb6a,stroke:#388e3c,color:#fff

Skillnaden förklarad:

Vänta - uppdateras verkligen "hela trädet" i traditionell JS?

Nej, det är en förenkling! När du gör:

document.querySelector("#counter").innerText = "Räknare: 1";

...så uppdateras bara den specifika noden. Men - här är varför Virtual DOM ändå ger fördelar:

Traditionell DOM-manipulation (flera uppdateringar):

// Varje rad triggar separat DOM-operation och potentiell repaint
document.querySelector("#name").innerText = "Anna";     // Operation 1
document.querySelector("#age").innerText = "25";        // Operation 2  
document.querySelector("#status").innerText = "Online"; // Operation 3

// Vid komplex logik - många manuella DOM-operationer
if (user.isLoggedIn) {
  document.querySelector("#login-btn").style.display = "none";
  document.querySelector("#user-menu").style.display = "block";
  document.querySelector("#username").innerText = user.name;
  // ... potentiellt 10+ fler DOM-operationer
}

Virtual DOM (React approach):

// React batchar alla dessa till EN DOM-uppdatering
function UserProfile({ user }) {
  return (
    <div>
      <span id="name">{user.name}</span>
      <span id="age">{user.age}</span>
      <span id="status">{user.isOnline ? "Online" : "Offline"}</span>
      {user.isLoggedIn ? (
        <UserMenu user={user} />
      ) : (
        <LoginButton />
      )}
    </div>
  );
}
// → React optimerar till minimal antal DOM-operationer

Virtual DOM:s verkliga fördelar:

  • Batching: Flera state-ändringar → en DOM-uppdatering
  • Smart diffing: Hoppar över onödiga uppdateringar (om värdet inte ändrats)
  • Förutsägbarhet: Deklarativ kod istället för imperativ DOM-manipulation
  • Komplexitet: Hanterar komplexa UI-förändringar elegant

Praktiskt exempel

Tänk dig denna React-komponent:

function Counter() {
  const [count, setCount] = useState(0);
  
  return (
    <div id="app">
      <header>
        <h1>Välkommen</h1>
      </header>
      <main>
        <div className="content">
          <p>Räknare: {count}</p> {/* Bara denna rad ändras */}
        </div>
        <button onClick={() => setCount(count + 1)}>
          Klicka
        </button>
      </main>
      <footer>Footer innehåll</footer>
    </div>
  );
}

När count ändras:

  1. Virtual DOM skapas med det nya värdet
  2. Diffing algoritm jämför gamla och nya Virtual DOM
  3. Minimal uppdatering - bara <p>-elementet uppdateras i Real DOM
  4. Resultat - snabb rendering utan onödig omritning

UI som funktion av state

Grunden i React är att se UI som en funktion av state: UI = f(state). Ett minimalt exempel:

import { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(c => c + 1)}>+1</button>
    </div>
  );
}

Skriva Markup med JSX

Varje React-komponent är en JavaScript-funktion som kan innehålla markup som React renderar i webbläsaren. React-komponenter använder en syntax-utökning som kallas JSX för att representera den markup:en.

JSX ser ut som HTML, men är lite striktare och kan visa dynamisk information. Om vi klistrar in befintlig HTML-markup i en React-komponent fungerar det inte alltid:

// Detta fungerar inte riktigt!
function TodoLista() {
  return (
    <h1>Hedy Lamarrs Todos</h1>
    <img 
      src="https://i.imgur.com/yXOvdOSs.jpg" 
      alt="Hedy Lamarr" 
      class="photo"
    >
    <ul>
      <li>Uppfinn nya trafikljus
      <li>Repetera en filmscen  
      <li>Förbättra spektrumteknologi
    </ul>
  );
}

Om du har befintlig HTML som detta kan du fixa det med en konverterare, eller följa JSX-reglerna:

function TodoLista() {
  return (
    <>
      <h1>Hedy Lamarrs Todos</h1>
      <img 
        src="https://i.imgur.com/yXOvdOSs.jpg" 
        alt="Hedy Lamarr" 
        className="photo"
      />
      <ul>
        <li>Uppfinn nya trafikljus</li>
        <li>Repetera en filmscen</li>
        <li>Förbättra spektrumteknologi</li>
      </ul>
    </>
  );
}

JavaScript i JSX med Klammerparenteser

JSX låter dig skriva HTML-liknande markup inuti en JavaScript-fil, vilket håller renderingslogik och innehåll på samma plats. Ibland vill du lägga till lite JavaScript-logik eller referera till en dynamisk egenskap inuti den markup:en. I denna situation kan du använda klammerparenteser i din JSX för att "öppna ett fönster" till JavaScript:

const person = {
  name: 'Gregorio Y. Zara',
  theme: {
    backgroundColor: 'black',
    color: 'pink'
  }
};

function TodoLista() {
  return (
    <div style={person.theme}>
      <h1>{person.name}s Todos</h1>
      <img
        className="avatar"
        src="https://i.imgur.com/7vQD0fPs.jpg"
        alt="Gregorio Y. Zara"
      />
      <ul>
        <li>Förbättra videotelefonen</li>
        <li>Förbered flygföreläsningar</li>
        <li>Arbeta på alkoholdrivna motorn</li>
      </ul>
    </div>
  );
}

Skicka Props till en Komponent

React-komponenter använder props för att kommunicera med varandra. Varje föräldrakomponent kan skicka information till sina barnkomponenter genom att ge dem props. Props kan påminna dig om HTML-attribut, men du kan skicka vilket JavaScript-värde som helst genom dem, inklusive objekt, arrayer, funktioner och till och med JSX!

function Avatar({ person, size }) {
  return (
    <img
      className="avatar"
      src={getImageUrl(person)}
      alt={person.name}
      width={size}
      height={size}
    />
  );
}

function Profil() {
  return (
    <div>
      <Avatar
        size={100}
        person={{ 
          name: 'Katsuko Saruhashi', 
          imageId: 'YfeOqp2' 
        }}
      />
    </div>
  );
}

Villkorlig Rendering

Dina komponenter behöver ofta visa olika saker beroende på olika villkor. I React kan du villkorligt rendera JSX med JavaScript-syntax som if-satser, && och ? : operatorer.

I detta exempel används JavaScript && operatorn för att villkorligt rendera en bockmarkering:

function Item({ name, isPacked }) {
  return (
    <li className="item">
      {name} {isPacked && '✅'}
    </li>
  );
}

function PackingList() {
  return (
    <section>
      <h1>Sally Rides Packlista</h1>
      <ul>
        <Item 
          isPacked={true} 
          name="Rymddräkt" 
        />
        <Item 
          isPacked={true} 
          name="Hjälm med gyllene blad" 
        />
        <Item 
          isPacked={false} 
          name="Foto av Tam" 
        />
      </ul>
    </section>
  );
}

Rendera Listor

Du vill ofta visa flera liknande komponenter från en samling data. Du kan använda JavaScripts map() med React för att transformera din dataarray till en array av komponenter.

För varje arrayobjekt behöver du specificera en key. Vanligtvis vill du använda ett ID från databasen som key. Keys låter React hålla reda på varje objekts plats i listan även om listan ändras.

const people = [
  { id: 0, name: 'Creola Katherine Johnson', profession: 'matematiker' },
  { id: 1, name: 'Mario José Molina-Pasquel', profession: 'kemist' },
  { id: 2, name: 'Mohammad Abdus Salam', profession: 'fysiker' },
];

function ScientistList() {
  const listItems = people.map(person =>
    <li key={person.id}>
      <p>
        <b>{person.name}</b> är en {person.profession}.
      </p>
    </li>
  );
  
  return (
    <article>
      <h1>Forskare</h1>
      <ul>{listItems}</ul>
    </article>
  );
}

Viktigt om keys:

  • Använd alltid en unik key för varje listelement
  • Keys hjälper React att förstå vilka element som ändrats
  • Använd aldrig array-index som key om listan kan ändras

Hålla Komponenter Rena

Vissa JavaScript-funktioner är rena. En ren funktion:

  • Sköter sina egna affärer. Den ändrar inte några objekt eller variabler som existerade innan den anropades.
  • Samma input, samma output. Givet samma input ska en ren funktion alltid returnera samma resultat.

Genom att strikt bara skriva dina komponenter som rena funktioner kan du undvika en hel klass av förvirrande buggar och oförutsägbart beteende när din kodbas växer. Här är ett exempel på en oren komponent:

let guest = 0;

function Cup() {
  // Dåligt: ändrar en redan existerande variabel!
  guest = guest + 1;
  return <h2>Tekopp för gäst #{guest}</h2>;
}

function TeaSet() {
  return (
    <>
      <Cup />
      <Cup />
      <Cup />
    </>
  );
}

Du kan göra denna komponent ren genom att skicka en prop istället för att modifiera en redan existerande variabel:

function Cup({ guest }) {
  return <h2>Tekopp för gäst #{guest}</h2>;
}

function TeaSet() {
  return (
    <>
      <Cup guest={1} />
      <Cup guest={2} />
      <Cup guest={3} />
    </>
  );
}

Utvecklingsmiljö: Kom Igång Snabbt

Snabbstart med Vite (Rekommenderat)

# Skapa nytt projekt med Vite
npm create vite@latest min-react-app -- --template react
cd min-react-app

# Installera dependencies
npm install

# Starta utvecklingsserver
npm run dev

Din Första React-app

När du har skapat projektet, öppna src/App.jsx och ersätt innehållet med:

function App() {
  return (
    <div>
      <h1>Min första React-app!</h1>
      <p>Välkommen till React-världen!</p>
    </div>
  );
}

export default App;

Prova att ändra texten och se hur sidan uppdateras direkt!

JSX-regler att komma ihåg

// 1. Måste ha ett parent element (eller React Fragment)
// ❌ Fel - flera root elements
function BadComponent() {
  return (
    <h1>Titel</h1>
    <p>Text</p>
  );
}

// ✅ Rätt - ett parent element
function GoodComponent() {
  return (
    <div>
      <h1>Titel</h1>
      <p>Text</p>
    </div>
  );
}

// ✅ Eller använd React Fragment
function GoodComponentFragment() {
  return (
    <>
      <h1>Titel</h1>
      <p>Text</p>
    </>
  );
}

// 2. JavaScript-uttryck inom {}
function DynamicComponent() {
  const products = ['Äpple', 'Banan', 'Citron'];
  const price = 25;

  return (
    <div>
      <h2>Produkter ({products.length})</h2>
      <p>Pris: {price} kr</p>
      <ul>
        {products.map(product => (
          <li key={product}>{product}</li>
        ))}
      </ul>
    </div>
  );
}

Utvecklingsmiljö: Kom Igång Snabbt

Snabbstart utan tooling

Vill du prova React direkt? Testa en minimal demo via CDN/online-sandbox (t.ex. StackBlitz):

<!doctype html>
<div id="root"></div>
<script type="module">
  import React from 'https://esm.sh/react';
  import ReactDOM from 'https://esm.sh/react-dom/client';

  function App() {
    return React.createElement('h1', null, 'Hej från React!');
  }

  const root = ReactDOM.createRoot(document.getElementById('root'));
  root.render(React.createElement(App));

  // För riktig utveckling: använd Vite (se nedan)
}</script>

Alternativ 1: Vite (Rekommenderat)

# Skapa nytt projekt med Vite
npm create vite@latest min-react-app -- --template react
cd min-react-app

# Installera dependencies
npm install

# Starta utvecklingsserver
npm run dev

Alternativ 2: Create React App (historiskt)

# Skapa nytt projekt
npx create-react-app min-react-app
cd min-react-app

# Starta utvecklingsserver
npm start

Projektstruktur (Create React App)

min-react-app/
  ├── public/
  │   ├── index.html        # HTML template
  │   └── favicon.ico
  ├── src/
  │   ├── App.js           # Main component
  │   ├── App.css          # Styles för App
  │   ├── index.js         # Entry point
  │   └── index.css        # Global styles
  ├── package.json         # Dependencies och scripts
  └── README.md

Din Första React-komponent

Låt oss titta på en enkel komponent:

// src/App.js
import './App.css';

function App() {
  const message = "Välkommen till React!";
  const currentYear = new Date().getFullYear();

  return (
    <div className="App">
      <header className="App-header">
        <h1>{message}</h1>
        <p>Året är {currentYear}</p>
        <button onClick={() => alert('Hej från React!')}>
          Klicka mig!
        </button>
      </header>
    </div>
  );
}

export default App;
// src/index.js - Entry point
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './index.css';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);

Notis:

  • Med den nya JSX-transformen (React 17+) behöver du inte längre import React from 'react' i varje komponentfil. Vissa mallar kan fortfarande inkludera importen – båda fungerar.
  • I React 18 kör StrictMode effekter två gånger i utvecklingsläge för att upptäcka biverkningar. Det påverkar inte produktion.

React Developer Tools

Installera React Developer Tools i din webbläsare:

Detta ger dig:

  • Komponentträd-visning
  • Props och state inspektion
  • Prestanda-profiling
  • Debugging-verktyg

Vad händer härnäst?

Nu har du lärt dig grunderna för att beskriva UI:t med React! I nästa avsnitt kommer vi att utforska:

  • Lägga till interaktivitet - hantera events och state
  • Hantera state - ge komponenter minne
  • Formulär - samla in användarinput
  • API-integration - hämta data från servrar

Är du redo? Gå vidare till nästa lektion för att lära dig hur du gör dina komponenter interaktiva!

Komponenter: Återanvändbara UI-byggstenar

Komponenter låter dig dela upp UI:t i oberoende, återanvändbara delar och tänka på varje del isolerat. Den här sidan ger en introduktion till idén om komponenter.

Mål: Lära dig skapa, organisera och återanvända komponenter för att bygga skalbar React-kod.

Definiera en Komponent

React-komponenter är JavaScript-funktioner som returnerar markup:

function MyButton() {
  return (
    <button>Jag är en knapp</button>
  );
}

Nu när du har deklarerat MyButton kan du nästla den i en annan komponent:

function MyApp() {
  return (
    <div>
      <h1>Välkommen till min app</h1>
      <MyButton />
    </div>
  );
}

Observera att <MyButton /> börjar med stor bokstav. Så här vet du att det är en React-komponent. React-komponentnamn måste alltid börja med stor bokstav, medan HTML-taggar måste vara små bokstäver.

Komponenter inom komponenter

Komponenter är vanliga JavaScript-funktioner, så du kan hålla flera komponenter i samma fil:

function Avatar() {
  return (
    <img
      className="avatar"
      src="https://i.imgur.com/1bX5QH6.jpg"
      alt="Lin Lanying"
      width={100}
      height={100}
    />
  );
}

function Profile() {
  return (
    <div>
      <Avatar />
      <Avatar />
      <Avatar />
    </div>
  );
}

I detta exempel har Profile-komponenten tre Avatar-komponenter.

Funktionella Komponenter: Det Moderna Sättet

Sedan React 16.8 och introduktionen av Hooks är funktionella komponenter standardsättet att skriva React-kod.

Grundläggande Komponent

// Enkel funktionell komponent
function Greeting() {
  return <h1>Hej världen!</h1>;
}

// Arrow function syntax (också vanlig)
const Greeting = () => {
  return <h1>Hej världen!</h1>;
};

// Kort syntax för enkel return
const Greeting = () => <h1>Hej världen!</h1>;

Komponent med Logik

function UserProfile() {
  const user = {
    name: "Anna Andersson",
    age: 28,
    email: "anna@example.com",
    avatar: "/images/anna.jpg"
  };

  const isAdult = user.age >= 18;

  return (
    <div className="user-profile">
      <img src={user.avatar} alt={`${user.name}s avatar`} />
      <h2>{user.name}</h2>
      <p>Ålder: {user.age} {isAdult && "✅ Myndig"}</p>
      <p>E-post: {user.email}</p>
    </div>
  );
}

Exportera och Importera Komponenter

Magin med komponenter ligger i deras återanvändbarhet: du kan skapa komponenter som består av andra komponenter. Men när du nästlar fler och fler komponenter är det ofta vettigt att börja dela upp dem i olika filer. Detta låter dig hålla dina filer lätta att skanna och återanvända komponenter på fler ställen.

// Gallery.js
import Profile from './Profile.js';

function Gallery() {
  return (
    <section>
      <h1>Fantastiska forskare</h1>
      <Profile />
      <Profile />
      <Profile />
    </section>
  );
}

export default Gallery;
// Profile.js
function Profile() {
  return (
    <img
      src="https://i.imgur.com/QIrZWGIs.jpg"
      alt="Alan L. Hart"
    />
  );
}

export default Profile;
// App.js
import Gallery from './Gallery.js';

function App() {
  return (
    <Gallery />
  );
}

export default App;

Komponentens Anatomi

En React-komponent består av några viktiga delar:

// 1. Import-statements (om du använder andra komponenter)
import { useState } from 'react';
import './Button.css';

// 2. Komponentfunktionen
function Button() {
  // 3. Logik (variabler, funktioner)
  const handleClick = () => {
    alert('Knappen klickades!');
  };
  
  // 4. Return-statement med JSX
  return (
    <button onClick={handleClick}>
      Klicka mig
    </button>
  );
}

// 5. Export-statement
export default Button;

Klasskomponenter: Det Äldre Sättet

Före hooks använde React klasskomponenter för att hantera state och lifecycle. Du kommer fortfarande stöta på dem i äldre kodbaser.

import React, { Component } from 'react';

class ClassCounter extends Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  // Lifecycle-metod
  componentDidMount() {
    console.log('Component har monterats');
  }

  componentDidUpdate(prevProps, prevState) {
    if (prevState.count !== this.state.count) {
      console.log(`Count ändrades från ${prevState.count} till ${this.state.count}`);
    }
  }

  componentWillUnmount() {
    console.log('Component kommer att avmonteras');
  }

  // Event handler
  incrementCount = () => {
    this.setState({ count: this.state.count + 1 });
  }

  render() {
    return (
      <div>
        <h2>Räknare: {this.state.count}</h2>
        <button onClick={this.incrementCount}>
          Öka
        </button>
      </div>
    );
  }
}

Organisera Komponenter

När din app växer är det viktigt att organisera komponenter på ett smart sätt:

Fil-per-komponent (Rekommenderat)

src/
  components/
    Button/
      Button.jsx
      Button.css
      Button.test.js
    Avatar/
      Avatar.jsx
      Avatar.css
    Card/
      Card.jsx
      Card.css
  pages/
    Home.jsx
    About.jsx
  App.jsx

Flera komponenter per fil (För små, relaterade komponenter)

// components/UI.jsx
export function Button({ children, onClick, type = "button" }) {
  return (
    <button type={type} onClick={onClick} className="btn">
      {children}
    </button>
  );
}

export function Input({ placeholder, value, onChange }) {
  return (
    <input 
      className="input"
      placeholder={placeholder}
      value={value}
      onChange={onChange}
    />
  );
}

export function Card({ children }) {
  return (
    <div className="card">
      {children}
    </div>
  );
}

Komponent-komposition

En av Reacts starkaste funktioner är komposition - att bygga komplexa komponenter från enklare komponenter:

function WelcomeMessage({ name }) {
  return <h1>Hej {name}!</h1>;
}

function UserAvatar({ imageUrl, name }) {
  return (
    <img 
      src={imageUrl} 
      alt={name}
      className="avatar" 
    />
  );
}

function UserCard({ user }) {
  return (
    <div className="user-card">
      <UserAvatar imageUrl={user.avatar} name={user.name} />
      <WelcomeMessage name={user.name} />
      <p>E-post: {user.email}</p>
    </div>
  );
}

// Användning
function App() {
  const user = {
    name: "Anna Andersson",
    email: "anna@example.com", 
    avatar: "/anna.jpg"
  };

  return <UserCard user={user} />;
}

Best Practices för Komponenter

1. Håll Komponenter Små och Fokuserade

// ❌ För stor komponent
function BlogPost({ post, user, comments, relatedPosts }) {
  return (
    <article>
      {/* Hundratals rader kod... */}
    </article>
  );
}

// ✅ Uppdelade komponenter
function BlogPost({ post }) {
  return (
    <article>
      <BlogHeader post={post} />
      <BlogContent content={post.content} />
      <BlogFooter postId={post.id} />
    </article>
  );
}

function BlogHeader({ post }) {
  return (
    <header>
      <h1>{post.title}</h1>
      <AuthorInfo author={post.author} />
      <PublishDate date={post.publishedAt} />
    </header>
  );
}

2. Använda PropTypes för Typkontroll (Optional)

PropTypes är ett verktyg för typkontroll i React som hjälper dig att:

  1. Hitta buggar tidigt: Genom att validera props under utveckling upptäcks fel direkt istället för att de dyker upp i produktion.

  2. Dokumentera komponenter: PropTypes fungerar som självdokumenterande kod - andra utvecklare kan snabbt se vilka props en komponent förväntar sig.

  3. Förbättra underhållbarhet: När du ändrar en komponent varnar PropTypes om du råkar skicka fel datatyp eller glömmer en required prop.

  4. Underlätta refaktorering: Med tydliga kontrakt mellan komponenter blir det säkrare att göra större kodändringar.

Exempel på vanliga PropTypes:

import PropTypes from 'prop-types';

function ProductCard({ name, price, image, onSale }) {
  return (
    <div className="product-card">
      <img src={image} alt={name} />
      <h3>{name}</h3>
      <p className={onSale ? "sale-price" : "regular-price"}>
        {price} kr
      </p>
    </div>
  );
}

ProductCard.propTypes = {
  name: PropTypes.string.isRequired,
  price: PropTypes.number.isRequired,
  image: PropTypes.string.isRequired,
  onSale: PropTypes.bool
};

ProductCard.defaultProps = {
  onSale: false
};

Ett vanligt alternativ till PropTypes är TypeScript som erbjuder statisk typkontroll under kompilering och ger bättre IDE-stöd med autocompletions och refaktoreringsmöjligheter. TypeScript är särskilt användbart i större projekt där PropTypes kan bli otillräckligt.

3. Komponentnamnskonventioner

// ✅ PascalCase för komponenter
function UserProfile() { }
function ProductCard() { }
function NavigationMenu() { }

// ✅ camelCase för funktioner och variabler
const handleClick = () => { };
const userName = "Anna";
const isLoading = true;

// ✅ Beskrivande namn
function LoadingSpinner() { }  // Bättre än Spinner()
function ErrorMessage() { }   // Bättre än Error()

Sammanfattning

Komponenter är byggstenen i React-applikationer:

  • Komponenter är JavaScript-funktioner som returnerar JSX
  • Komponentnamn måste börja med stor bokstav
  • Import/Export låter dig organisera komponenter i separata filer
  • Komposition bygger komplexa UI från enkla komponenter
  • Håll komponenter små och fokuserade på en sak

Vad händer härnäst?

Nu när du kan skapa komponenter är det dags att göra dem interaktiva! I nästa avsnitt lär du dig:

  • Props - skicka data mellan komponenter
  • State - ge komponenter minne
  • Events - reagera på användarinteraktion

Gå vidare till State och Props för att lära dig hur du gör dina komponenter levande!

Lägga till Interaktivitet: Props, State och Events

Komponenter behöver ofta interagera med varandra och reagera på användarinteraktion. React använder props för att skicka data och state för att komma ihåg saker. Tillsammans gör de dina komponenter interaktiva!

Mål: Lära sig skicka data med props, hantera användarinteraktion med events och ge komponenter minne med state.

Skicka Data med Props

React-komponenter använder props för att kommunicera med varandra. Varje föräldrakomponent kan skicka information till sina barnkomponenter genom att ge dem props.

Grundläggande Props

// Komponent som tar emot props
function Avatar({ person, size }) {
  return (
    <img
      className="avatar"
      src={person.imageUrl}
      alt={person.name}
      width={size}
      height={size}
    />
  );
}

// Föräldrakomponent som skickar props
function Profile() {
  return (
    <Avatar
      size={100}
      person={{ 
        name: 'Katsuko Saruhashi', 
        imageUrl: 'https://i.imgur.com/YfeOqp2s.jpg' 
      }}
    />
  );
}

Props kan vara vilken datatyp som helst

function Card({ title, children, isHighlighted, tags, onClose }) {
  return (
    <div className={`card ${isHighlighted ? 'highlighted' : ''}`}>
      <div className="card-header">
        <h3>{title}</h3>
        <button onClick={onClose}>×</button>
      </div>
      <div className="card-body">
        {children}
      </div>
      <div className="card-footer">
        {tags.map(tag => (
          <span key={tag} className="tag">{tag}</span>
        ))}
      </div>
    </div>
  );
}

// Användning - skicka olika datatyper som props
function App() {
  return (
    <Card
      title="Min artikel"                    // string
      isHighlighted={true}                   // boolean
      tags={['react', 'javascript', 'web']} // array
      onClose={() => alert('Stänger!')}     // function
    >
      <p>Detta är innehållet i kortet.</p>   {/* children */}
    </Card>
  );
}

Reagera på Events

Du kan reagera på events genom att deklarera event handler-funktioner inuti dina komponenter:

function Button() {
  function handleClick() {
    alert('Du klickade på mig!');
  }

  return (
    <button onClick={handleClick}>
      Klicka mig
    </button>
  );
}

Observera att onClick={handleClick} inte har parenteser i slutet! Anropa inte event handler-funktionen: du behöver bara skicka den nedåt. React kommer att anropa din event handler när användaren klickar på knappen.

Event Handlers kan vara inline

function Button() {
  return (
    <button onClick={() => alert('Du klickade på mig!')}>
      Klicka mig
    </button>
  );
}

Skicka Event Handlers som Props

Ofta vill du att föräldrakomponenten ska specificera en barnkomponents event handler:

function Button({ onClick, children }) {
  return (
    <button onClick={onClick}>
      {children}
    </button>
  );
}

function PlayButton({ movieName }) {
  function handlePlayClick() {
    alert(`Spelar ${movieName}!`);
  }

  return (
    <Button onClick={handlePlayClick}>
      Spela "{movieName}"
    </Button>
  );
}

function UploadButton() {
  return (
    <Button onClick={() => alert('Laddar upp!')}>
      Ladda upp bild
    </Button>
  );
}

State: En Komponents Minne

Komponenter behöver ofta ändra vad som visas på skärmen som resultat av interaktion. Att skriva i formuläret bör uppdatera input-fältet, att klicka på "nästa" i en bildkarusell bör ändra vilken bild som visas, att klicka på "köp" bör lägga en produkt i kundvagnen. Komponenter behöver "komma ihåg" saker: det aktuella input-värdet, den aktuella bilden, kundvagnen. I React kallas denna typ av komponentspecifikt minne state.

Lägga till State i en Komponent

För att lägga till state i en komponent, importera useState från React:

import { useState } from 'react';

function MyButton() {
  const [count, setCount] = useState(0);

  function handleClick() {
    setCount(count + 1);
  }

  return (
    <button onClick={handleClick}>
      Klickad {count} gånger
    </button>
  );
}

useState returnerar två saker:

  1. Det aktuella state-värdet (count)
  2. En funktion för att uppdatera det (setCount)

Du kan ge dem vilka namn du vill, men konventionen är att skriva [something, setSomething].

State är isolerat och privat

State är lokalt för en komponentinstans på skärmen. Med andra ord, om du renderar samma komponent två gånger får varje kopia sin egen state:

function MyApp() {
  return (
    <div>
      <h1>Räknare som uppdateras oberoende</h1>
      <MyButton />
      <MyButton />
    </div>
  );
}

Observera hur varje knapp "kommer ihåg" sin egen count-state och inte påverkar den andra.

Olika Typer av State

State kan innehålla vilken typ av JavaScript-värde som helst:

import { useState } from 'react';

function Form() {
  // Olika typer av state
  const [name, setName] = useState('');           // string
  const [age, setAge] = useState(0);              // number  
  const [isSubscribed, setIsSubscribed] = useState(false); // boolean
  const [hobbies, setHobbies] = useState([]);     // array
  
  return (
    <div>
      <input 
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="Ditt namn"
      />
      <p>Hej {name}!</p>
      
      <input 
        type="number"
        value={age}
        onChange={(e) => setAge(Number(e.target.value))}
        placeholder="Din ålder"
      />
      <p>Du är {age} år gammal.</p>
      
      <label>
        <input 
          type="checkbox"
          checked={isSubscribed}
          onChange={(e) => setIsSubscribed(e.target.checked)}
        />
        Prenumerera på nyhetsbrev
      </label>
      <p>{isSubscribed ? 'Du är prenumerant!' : 'Du är inte prenumerant.'}</p>
    </div>
  );
}

State och Props Tillsammans

Ofta använder du state och props tillsammans. Här är ett exempel där en föräldrakomponent håller state och skickar det till barnkomponenter via props:

function Counter() {
  const [count, setCount] = useState(0);

  function handleIncrement() {
    setCount(count + 1);
  }

  return (
    <div>
      <h2>Räknare</h2>
      <CounterDisplay count={count} />
      <CounterButton onClick={handleIncrement} />
    </div>
  );
}

function CounterDisplay({ count }) {
  return <p>Aktuellt värde: {count}</p>;
}

function CounterButton({ onClick }) {
  return (
    <button onClick={onClick}>
      Öka med 1
    </button>
  );
}

I detta exempel:

  • Counter äger state (count)
  • CounterDisplay får count via props
  • CounterButton får onClick funktionen via props
  • När knappen klickas uppdateras state, vilket orsakar en re-render

State Batching och Funktionella Updates

function AdvancedCounter() {
  const [count, setCount] = useState(0);

  // ❌ Problematiskt - baserat på nuvarande värde
  const incrementTwice = () => {
    setCount(count + 1);
    setCount(count + 1); // Fortfarande baserat på samma värde!
  };

  // ✅ Bättre - funktionell update
  const incrementTwiceProperly = () => {
    setCount(prev => prev + 1);
    setCount(prev => prev + 1); // Använder uppdaterat värde
  };

  // ✅ Batch updates automatiskt
  const handleMultipleUpdates = () => {
    setCount(prev => prev + 1);
    // React batchar dessa tillsammans
    console.log('This will run after all updates');
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={incrementTwice}>+2 (Fel)</button>
      <button onClick={incrementTwiceProperly}>+2 (Rätt)</button>
    </div>
  );
}

useEffect Hook: Side Effects och Lifecycle

useEffect hanterar "side effects" - allt som inte är direkt kopplat till rendering.

Grundläggande useEffect Patterns

import { useState, useEffect } from 'react';

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  // Pattern 1: Kör efter varje render
  useEffect(() => {
    console.log('Component rendered');
  });

  // Pattern 2: Kör bara en gång (componentDidMount)
  useEffect(() => {
    console.log('Component mounted');
  }, []); // Tom dependency array

  // Pattern 3: Kör när specifika värden ändras (med AbortController)
  useEffect(() => {
    if (!userId) return;

    const controller = new AbortController();
    const { signal } = controller;

    const fetchUser = async () => {
      try {
        setLoading(true);
        setError(null);
        
        const response = await fetch(`/api/users/${userId}`, { signal });
        if (!response.ok) throw new Error('Användare hittades inte');
        
        const userData = await response.json();
        setUser(userData);
      } catch (err) {
        if (err.name !== 'AbortError') {
          setError(err.message);
        }
      } finally {
        setLoading(false);
      }
    };

    fetchUser();
    return () => controller.abort();
  }, [userId]); // Kör när userId ändras

  // Pattern 4: Cleanup (componentWillUnmount)
  useEffect(() => {
    const timer = setInterval(() => {
      console.log('Timer tick');
    }, 1000);

    // Cleanup function
    return () => {
      clearInterval(timer);
      console.log('Timer cleared');
    };
  }, []);

  if (loading) return <div>Laddar...</div>;
  if (error) return <div>Fel: {error}</div>;
  if (!user) return <div>Ingen användare vald</div>;

  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
}

useEffect Best Practices

function SearchComponent() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    // Undvik onödiga API-anrop
    if (!query.trim()) {
      setResults([]);
      return;
    }

    const searchTimer = setTimeout(async () => {
      setLoading(true);
      
      try {
        const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
        const data = await response.json();
        setResults(data.results);
      } catch (error) {
        console.error('Search failed:', error);
        setResults([]);
      } finally {
        setLoading(false);
      }
    }, 300); // Debounce 300ms

    // Cleanup - avbryt föregående timer
    return () => clearTimeout(searchTimer);
  }, [query]);

  return (
    <div>
      <input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Sök..."
      />
      
      {loading && <p>Söker...</p>}
      
      <ul>
        {results.map(result => (
          <li key={result.id}>{result.title}</li>
        ))}
      </ul>
    </div>
  );
}

Observera: aktivera ESLint-regeln react-hooks/exhaustive-deps för att fånga saknade beroenden i hooks och undvika subtila buggar.

Props Drilling: Problem och Lösningar

Props drilling uppstår när data behöver passas genom många komponentnivåer.

// ❌ Props drilling problem
function App() {
  const [user, setUser] = useState({ name: 'Anna', theme: 'dark' });
  
  return (
    <div>
      <Header user={user} setUser={setUser} />
      <Main user={user} setUser={setUser} />
    </div>
  );
}

function Header({ user, setUser }) {
  return (
    <header>
      <Logo />
      <Navigation user={user} setUser={setUser} />
    </header>
  );
}

function Navigation({ user, setUser }) {
  return (
    <nav>
      <UserMenu user={user} setUser={setUser} />
    </nav>
  );
}

function UserMenu({ user, setUser }) {
  const toggleTheme = () => {
    setUser(prev => ({
      ...prev,
      theme: prev.theme === 'light' ? 'dark' : 'light'
    }));
  };

  return (
    <div>
      <span>Hej {user.name}!</span>
      <button onClick={toggleTheme}>
        Tema: {user.theme}
      </button>
    </div>
  );
}

Context API: Global State Management

Context API löser props drilling genom att skapa en "global" state som komponenter kan accessa direkt.

import { createContext, useContext, useState } from 'react';

// 1. Skapa Context
const UserContext = createContext();

// 2. Skapa Provider Component
function UserProvider({ children }) {
  const [user, setUser] = useState({
    name: 'Anna',
    theme: 'light',
    isAuthenticated: false
  });

  const login = (userData) => {
    setUser(prev => ({
      ...prev,
      ...userData,
      isAuthenticated: true
    }));
  };

  const logout = () => {
    setUser({
      name: '',
      theme: 'light',
      isAuthenticated: false
    });
  };

  const toggleTheme = () => {
    setUser(prev => ({
      ...prev,
      theme: prev.theme === 'light' ? 'dark' : 'light'
    }));
  };

  const value = {
    user,
    login,
    logout,
    toggleTheme
  };

  return (
    <UserContext.Provider value={value}>
      {children}
    </UserContext.Provider>
  );
}

// 3. Custom hook för att använda context
function useUser() {
  const context = useContext(UserContext);
  
  if (!context) {
    throw new Error('useUser must be used within UserProvider');
  }
  
  return context;
}

// 4. Använd Context i komponenter
function App() {
  return (
    <UserProvider>
      <div className="app">
        <Header />
        <Main />
        <Footer />
      </div>
    </UserProvider>
  );
}

function Header() {
  return (
    <header>
      <Logo />
      <Navigation />
    </header>
  );
}

// Ingen props drilling längre!
function Navigation() {
  const { user, toggleTheme, logout } = useUser();

  return (
    <nav>
      <span>Hej {user.name}!</span>
      <button onClick={toggleTheme}>
        Tema: {user.theme}
      </button>
      {user.isAuthenticated && (
        <button onClick={logout}>
          Logga ut
        </button>
      )}
    </nav>
  );
}

function Main() {
  const { user } = useUser();
  
  return (
    <main className={`theme-${user.theme}`}>
      {user.isAuthenticated ? (
        <Dashboard />
      ) : (
        <LoginForm />
      )}
    </main>
  );
}

function LoginForm() {
  const { login } = useUser();
  const [formData, setFormData] = useState({ name: '', email: '' });

  const handleSubmit = (e) => {
    e.preventDefault();
    login(formData);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={formData.name}
        onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
        placeholder="Namn"
      />
      <input
        type="email"
        value={formData.email}
        onChange={(e) => setFormData(prev => ({ ...prev, email: e.target.value }))}
        placeholder="E-post"
      />
      <button type="submit">Logga in</button>
    </form>
  );
}

Avancerade State Patterns

Custom Hooks för State Logic

// Custom hook för formulärhantering
function useForm(initialValues) {
  const [values, setValues] = useState(initialValues);
  const [errors, setErrors] = useState({});

  const setValue = (name, value) => {
    setValues(prev => ({ ...prev, [name]: value }));
    // Rensa fel när användaren rättar
    if (errors[name]) {
      setErrors(prev => ({ ...prev, [name]: '' }));
    }
  };

  const setError = (name, error) => {
    setErrors(prev => ({ ...prev, [name]: error }));
  };

  const reset = () => {
    setValues(initialValues);
    setErrors({});
  };

  return {
    values,
    errors,
    setValue,
    setError,
    reset
  };
}

// Custom hook för localStorage
function useLocalStorage(key, defaultValue) {
  const [value, setValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : defaultValue;
    } catch (error) {
      console.error(`Error reading localStorage key "${key}":`, error);
      return defaultValue;
    }
  });

  const setStoredValue = (newValue) => {
    try {
      setValue(newValue);
      window.localStorage.setItem(key, JSON.stringify(newValue));
    } catch (error) {
      console.error(`Error setting localStorage key "${key}":`, error);
    }
  };

  return [value, setStoredValue];
}

// Användning av custom hooks
function UserPreferences() {
  const { values, setValue, errors, setError } = useForm({
    theme: 'light',
    language: 'sv',
    notifications: true
  });

  const [settings, setSettings] = useLocalStorage('userSettings', values);

  const handleSave = () => {
    if (!values.language) {
      setError('language', 'Språk är obligatoriskt');
      return;
    }

    setSettings(values);
    alert('Inställningar sparade!');
  };

  return (
    <div>
      <h2>Användarinställningar</h2>
      
      <select
        value={values.theme}
        onChange={(e) => setValue('theme', e.target.value)}
      >
        <option value="light">Ljust tema</option>
        <option value="dark">Mörkt tema</option>
      </select>

      <select
        value={values.language}
        onChange={(e) => setValue('language', e.target.value)}
      >
        <option value="">Välj språk</option>
        <option value="sv">Svenska</option>
        <option value="en">Engelska</option>
      </select>
      {errors.language && <span className="error">{errors.language}</span>}

      <label>
        <input
          type="checkbox"
          checked={values.notifications}
          onChange={(e) => setValue('notifications', e.target.checked)}
        />
        Aktivera notifikationer
      </label>

      <button onClick={handleSave}>Spara inställningar</button>
    </div>
  );
}

Best Practices för State Management

1. Håll State så Lokalt som Möjligt

// ❌ Onödig global state
function App() {
  const [modalOpen, setModalOpen] = useState(false);
  const [modalContent, setModalContent] = useState('');
  
  return (
    <div>
      <Header />
      <Main modalOpen={modalOpen} setModalOpen={setModalOpen} />
      {modalOpen && <Modal content={modalContent} />}
    </div>
  );
}

// ✅ Lokal state där den behövs
function ProductCard({ product }) {
  const [showDetails, setShowDetails] = useState(false);
  
  return (
    <div className="product-card">
      <h3>{product.name}</h3>
      <button onClick={() => setShowDetails(!showDetails)}>
        {showDetails ? 'Dölj' : 'Visa'} detaljer
      </button>
      {showDetails && <ProductDetails product={product} />}
    </div>
  );
}

2. Normalisera Komplex State

// ❌ Nested state structure
const [state, setState] = useState({
  posts: [
    {
      id: 1,
      title: 'Post 1',
      author: { id: 1, name: 'Anna' },
      comments: [
        { id: 1, text: 'Bra inlägg!', author: { id: 2, name: 'Erik' } }
      ]
    }
  ]
});

// ✅ Normalized state structure
const [state, setState] = useState({
  posts: { 1: { id: 1, title: 'Post 1', authorId: 1, commentIds: [1] } },
  authors: { 1: { id: 1, name: 'Anna' }, 2: { id: 2, name: 'Erik' } },
  comments: { 1: { id: 1, text: 'Bra inlägg!', authorId: 2 } }
});

Sammanfattning

Nu kan du skapa interaktiva React-komponenter:

  • Props skickar data från föräldra- till barnkomponenter
  • Event handlers låter komponenter reagera på användarinteraktion
  • State ger komponenter minne med useState
  • State är privat - varje komponentinstans har sin egen state
  • State + Props skapar dataflöde mellan komponenter

Vad händer härnäst?

Nu när dina komponenter kan ta emot data och reagera på interaktion är det dags att bygga riktiga applikationer! I nästa avsnitt lär du dig:

  • Formulär - hantera användarinput på ett kontrollerat sätt
  • API-integration - hämta data från servrar
  • Routing - navigera mellan olika vyer
  • Deployment - publicera din app på internet

Redo för nästa steg? Gå vidare till Formulär i React för att lära dig hantera användarinput!


Fördjupning: Avancerade State-patterns

Denna sektion täcker mer avancerade ämnen som du kan hoppa över först och komma tillbaka till senare.

Routing med React Router: Navigation i SPA:er

I traditionella webbapplikationer hanteras navigation av servern - varje URL motsvarar en specifik fil eller endpoint. I Single Page Applications behöver vi client-side routing för att hantera navigation utan att ladda om hela sidan.

React Router är det de facto-standardbiblioteket för routing i React-applikationer.

Mål: Lära sig React Router, implementera BrowserRouter, konfigurera routes, hantera navigation programmatiskt och skapa protected routes.

Installation och Grundkonfiguration

Installation

npm install react-router-dom

Grundläggande Router Setup

// App.js
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import Layout from './components/Layout';
import Home from './pages/Home';
import About from './pages/About';
import Contact from './pages/Contact';
import NotFound from './pages/NotFound';

function App() {
  return (
    <Router>
      <Layout>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
          <Route path="/contact" element={<Contact />} />
          <Route path="*" element={<NotFound />} />
        </Routes>
      </Layout>
    </Router>
  );
}

export default App;

Router-typer

React Router erbjuder flera router-typer för olika användningsfall:

graph TD
    A[React Router] --> B[BrowserRouter]
    A --> C[HashRouter]
    A --> D[MemoryRouter]
    
    B --> E["Clean URLs<br/>/about, /products"]
    C --> F["Hash URLs<br/>/#/about, /#/products"]
    D --> G["Memory only<br/>(testing, React Native)"]
    
    style B fill:#98fb98
    style E fill:#98fb98

BrowserRouter (Rekommenderad)

import { BrowserRouter } from 'react-router-dom';

// Ger rena URLs: example.com/about
function App() {
  return (
    <BrowserRouter>
      {/* Routes här */}
    </BrowserRouter>
  );
}

HashRouter (Fallback)

import { HashRouter } from 'react-router-dom';

// Ger hash URLs: example.com/#/about
// Användbart för statiska hostar som GitHub Pages
function App() {
  return (
    <HashRouter>
      {/* Routes här */}
    </HashRouter>
  );
}

Routes och Route Configuration

Grundläggande Routes

import { Routes, Route } from 'react-router-dom';

function AppRoutes() {
  return (
    <Routes>
      {/* Exact match för hem-sidan */}
      <Route path="/" element={<Home />} />
      
      {/* Enkla routes */}
      <Route path="/about" element={<About />} />
      <Route path="/services" element={<Services />} />
      <Route path="/contact" element={<Contact />} />
      
      {/* 404 - måste vara sist */}
      <Route path="*" element={<NotFound />} />
    </Routes>
  );
}

Nested Routes (Nästlade routes)

// Huvudroutes
function App() {
  return (
    <Router>
      <Routes>
        <Route path="/" element={<Layout />}>
          {/* Child routes */}
          <Route index element={<Home />} />
          <Route path="about" element={<About />} />
          <Route path="products/*" element={<ProductRoutes />} />
        </Route>
      </Routes>
    </Router>
  );
}

// Layout-komponent med Outlet
import { Outlet } from 'react-router-dom';

function Layout() {
  return (
    <div>
      <header>
        <Navigation />
      </header>
      <main>
        <Outlet /> {/* Här renderas child routes */}
      </main>
      <footer>
        <Footer />
      </footer>
    </div>
  );
}

// Produktroutes som separata komponenter (v6, relativa paths)
function ProductRoutes() {
  return (
    <Routes>
      <Route index element={<ProductList />} />
      <Route path=":id" element={<ProductDetail />} />
      <Route path="categories" element={<ProductCategories />} />
      <Route path="categories/:category" element={<CategoryProducts />} />
    </Routes>
  );
}

Dynamiska Routes med Parameters

import { useParams } from 'react-router-dom';

// Route configuration
<Route path="/users/:userId" element={<UserProfile />} />
<Route path="/posts/:postId/comments/:commentId" element={<Comment />} />

// I komponenten
function UserProfile() {
  const { userId } = useParams();
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(setUser);
  }, [userId]);

  if (!user) return <div>Laddar...</div>;

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

// Query parameters
function ProductList() {
  const [searchParams, setSearchParams] = useSearchParams();
  const category = searchParams.get('category');
  const sort = searchParams.get('sort');

  const updateSearch = (newCategory) => {
    setSearchParams({ category: newCategory, sort });
  };

  return (
    <div>
      <h2>Produkter {category && `i kategorin ${category}`}</h2>
      <button onClick={() => updateSearch('electronics')}>
        Elektronik
      </button>
    </div>
  );
}
import { Link } from 'react-router-dom';

function Navigation() {
  return (
    <nav>
      {/* Grundläggande länkar */}
      <Link to="/">Hem</Link>
      <Link to="/about">Om oss</Link>
      <Link to="/products">Produkter</Link>
      
      {/* Dynamiska länkar */}
      <Link to={`/users/${user.id}`}>Min profil</Link>
      
      {/* Med state (för att skicka data) */}
      <Link 
        to="/checkout" 
        state={{ from: 'cart', items: cartItems }}
      >
        Till kassan
      </Link>
      
      {/* Ersätt current history entry */}
      <Link to="/login" replace>
        Logga in
      </Link>
    </nav>
  );
}
import { NavLink } from 'react-router-dom';

function Navigation() {
  return (
    <nav>
      <NavLink 
        to="/" 
        className={({ isActive }) => isActive ? 'active' : ''}
      >
        Hem
      </NavLink>
      
      <NavLink 
        to="/products" 
        style={({ isActive }) => ({
          color: isActive ? 'red' : 'blue'
        })}
      >
        Produkter
      </NavLink>
      
      {/* Med custom active class (v6) */}
      <NavLink 
        to="/about"
        className={({ isActive }) => `nav-link ${isActive ? 'current' : ''}`}
      >
        Om oss
      </NavLink>
    </nav>
  );
}

Programmatisk Navigation (varför)

import { useNavigate, useLocation } from 'react-router-dom';

function LoginForm() {
  const navigate = useNavigate();
  const location = useLocation();

  const handleLogin = async (credentials) => {
    try {
      await login(credentials);
      
      // Navigera tillbaka till föregående sida eller hem
      const from = location.state?.from?.pathname || '/';
      navigate(from, { replace: true });
    } catch (error) {
      setError('Inloggning misslyckades');
    }
  };

  const goBack = () => {
    navigate(-1); // Gå tillbaka i historiken
  };

  const goToProducts = () => {
    navigate('/products', { 
      state: { from: 'login' },
      replace: false 
    });
  };

  return (
    <form onSubmit={handleLogin}>
      {/* Formulärfält */}
      <button type="button" onClick={goBack}>
        Tillbaka
      </button>
      <button type="submit">
        Logga in
      </button>
    </form>
  );
}

Vidare läsning

Fler routing‑ämnen när du är redo: skyddade routes (auth), data‑laddare, central felhantering, och animerade övergångar. Dessa bör komma efter att grunderna sitter.

import { createBrowserRouter, RouterProvider } from 'react-router-dom';

const router = createBrowserRouter([
  {
    path: '/',
    element: <Layout />,
    errorElement: <ErrorPage />,
    children: [
      {
        index: true,
        element: <Home />
      },
      {
        path: 'products',
        element: <ProductsLayout />,
        children: [
          {
            index: true,
            element: <ProductList />
          },
          {
            path: ':id',
            element: <ProductDetail />,
            loader: async ({ params }) => {
              return fetch(`/api/products/${params.id}`);
            }
          }
        ]
      },
      {
        path: 'admin',
        element: <ProtectedRoute requiredRole="admin"><AdminLayout /></ProtectedRoute>,
        children: [
          {
            path: 'users',
            element: <UserManagement />
          },
          {
            path: 'settings',
            element: <Settings />
          }
        ]
      }
    ]
  }
]);

function App() {
  return <RouterProvider router={router} />;
}
// Modern data loading pattern (React Router v6.4+)
const router = createBrowserRouter([
  {
    path: '/products/:id',
    element: <ProductDetail />,
    loader: async ({ params }) => {
      const product = await fetch(`/api/products/${params.id}`);
      if (!product.ok) {
        throw new Response('Product not found', { status: 404 });
      }
      return product.json();
    },
    errorElement: <ProductError />
  }
]);

// I komponenten
function ProductDetail() {
  const product = useLoaderData();
  
  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
    </div>
  );
}
import { AnimatePresence, motion } from 'framer-motion';
import { useLocation } from 'react-router-dom';

function AnimatedRoutes() {
  const location = useLocation();

  return (
    <AnimatePresence mode="wait">
      <Routes location={location} key={location.pathname}>
        <Route 
          path="/" 
          element={
            <motion.div
              initial={{ opacity: 0, x: -100 }}
              animate={{ opacity: 1, x: 0 }}
              exit={{ opacity: 0, x: 100 }}
              transition={{ duration: 0.3 }}
            >
              <Home />
            </motion.div>
          } 
        />
        {/* Andra routes... */}
      </Routes>
    </AnimatePresence>
  );
}

Error Boundaries för Routes

class RouteErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    console.error('Route error:', error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return (
        <div className="error-page">
          <h2>Något gick fel</h2>
          <p>Vi ber om ursäkt för besväret.</p>
          <Link to="/">Gå tillbaka till startsidan</Link>
        </div>
      );
    }

    return this.props.children;
  }
}

// 404 Component
function NotFound() {
  const location = useLocation();

  return (
    <div className="not-found">
      <h1>404 - Sidan hittades inte</h1>
      <p>Sidan <code>{location.pathname}</code> existerar inte.</p>
      <Link to="/">Gå till startsidan</Link>
    </div>
  );
}

Best Practices

1. Route Organization

// routes/index.js - Centraliserad route configuration
export const routes = {
  home: '/',
  about: '/about',
  products: '/products',
  productDetail: (id) => `/products/${id}`,
  userProfile: (userId) => `/users/${userId}`,
  admin: {
    base: '/admin',
    users: '/admin/users',
    settings: '/admin/settings'
  }
};

// Användning
<Link to={routes.productDetail(product.id)}>
  {product.name}
</Link>

2. Route Guards

// Reusable route guard component
function RouteGuard({ 
  children, 
  requireAuth = false, 
  requiredRole = null,
  fallback = null 
}) {
  const { user, loading } = useAuth();
  const location = useLocation();

  if (loading) return <LoadingSpinner />;

  if (requireAuth && !user) {
    return <Navigate to="/login" state={{ from: location }} replace />;
  }

  if (requiredRole && (!user || user.role !== requiredRole)) {
    return fallback || <Navigate to="/unauthorized" replace />;
  }

  return children;
}

3. SEO-vänliga Routes

// Dynamiska titles baserat på route
function useDocumentTitle(title) {
  useEffect(() => {
    const prevTitle = document.title;
    document.title = title;
    return () => {
      document.title = prevTitle;
    };
  }, [title]);
}

function ProductDetail() {
  const { id } = useParams();
  const [product, setProduct] = useState(null);

  useDocumentTitle(product ? `${product.name} - Min Butik` : 'Laddar...');

  // ...
}

Sammanfattning

React Router är kraftfullt verktyg för navigation i React-applikationer:

  • BrowserRouter ger rena URLs och är bästa valet för de flesta applikationer
  • Routes och Route konfigurerar URL-mappningar till komponenter
  • Link och NavLink skapar navigerbara länkar
  • useNavigate möjliggör programmatisk navigation
  • Protected Routes skyddar känsligt innehåll
  • Dynamic routes hanterar parametrar och query strings

Nästa steg är att lära sig konsumera API:er för att hämta och skicka data till backend-tjänster.

Formulär i React: Interaktiv Användarinmatning

Problemet: Statisk HTML räcker inte

Du har byggt en fin statisk webbsida, men nu vill användarna kunna skicka meddelanden, registrera konton eller söka efter produkter. Vanlig HTML-formulär skickar dig till en ny sida - inte så användarvänligt för moderna webbappar.

<!-- Traditionellt HTML-formulär -->
<form action="/submit" method="POST">
  <input type="text" name="username" />
  <button type="submit">Skicka</button>
</form>
<!-- Sidan laddas om och användaren förlorar sitt sammanhang -->

Problem:

  • Sidan laddas om vid varje submit
  • Ingen validering innan skickning
  • Svårt att ge användaren feedback
  • Kan inte kombinera med andra React-komponenter smidigt

Lösningen: Kontrollerade formulär med React

React gör formulär interaktiva genom att låta oss hantera all input i JavaScript och ge omedelbar feedback utan sidladdningar.

Grundprincipen

I React kontrollerar vi formulär-värden med useState och hanterar ändringar med event handlers. Detta kallas "controlled components".

Steg-för-steg: Bygg interaktiva formulär

Steg 1: Ett enkelt textfält

Låt oss börja med det allra enklaste - en komponent som visar vad användaren skriver medan de skriver:

import { useState } from 'react';

function SimpleForm() {
  const [name, setName] = useState('');

  return (
    <div>
      <h2>Hej, {name || 'okänd person'}!</h2>
      
      <input 
        type="text"
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="Skriv ditt namn..."
      />
      
      <p>Du har skrivit: "{name}"</p>
    </div>
  );
}

Prova detta: Skriv i fältet och se hur texten uppdateras direkt medan du skriver!

Viktiga delar:

  • value={name} - React kontrollerar vad som visas
  • onChange - Körs varje gång användaren skriver
  • e.target.value - Det nya värdet från input-fältet

Steg 2: Hantera formulär-submit

Nu lägger vi till en submit-knapp och förhindrar sidladdning:

function ContactForm() {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault(); // Förhindra sidladdning!
    
    console.log('Formulär skickat:', { name, email });
    alert(`Tack ${name}! Vi kontaktar dig på ${email}`);
  };

  return (
    <form onSubmit={handleSubmit} className="contact-form">
      <h2>Kontakta oss</h2>
      
      <input 
        type="text"
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="Ditt namn"
        className="form-input"
      />
      
      <input 
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Din e-post"
        className="form-input"
      />
      
      <button type="submit" className="submit-button">
        Skicka meddelande
      </button>
    </form>
  );
}

Förslag CSS

.contact-form {
  max-width: 400px;
  margin: 0 auto;
  padding: 20px;
}

.form-input {
  width: 100%;
  padding: 10px;
  margin: 10px 0;
  border: 1px solid #ddd;
  border-radius: 4px;
  box-sizing: border-box;
}

.submit-button {
  width: 100%;
  padding: 12px;
  background-color: #007bff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

Testa: Fyll i formuläret och klicka "Skicka meddelande"

Steg 3: Lägg till validering

Nu gör vi formuläret smartare med validering:

function ValidatedForm() {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');
  const [errors, setErrors] = useState({});

  const validateForm = () => {
    const newErrors = {};
    
    if (!name.trim()) {
      newErrors.name = 'Namn är obligatoriskt';
    }
    
    if (!email.trim()) {
      newErrors.email = 'E-post är obligatoriskt';
    } else if (!email.includes('@')) {
      newErrors.email = 'Ange en giltig e-postadress';
    }
    
    return newErrors;
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    
    const formErrors = validateForm();
    setErrors(formErrors);
    
    if (Object.keys(formErrors).length === 0) {
      // Inga fel - skicka formuläret
      console.log('Formulär godkänt:', { name, email });
      alert('Meddelande skickat!');
      
      // Rensa formuläret
      setName('');
      setEmail('');
    }
  };

  return (
    <form onSubmit={handleSubmit} className="validated-form">
      <h2>Kontakta oss</h2>
      
      <div className="form-group">
        <input 
          type="text"
          value={name}
          onChange={(e) => setName(e.target.value)}
          placeholder="Ditt namn"
          className={`form-input ${errors.name ? 'error' : ''}`}
        />
        {errors.name && <span className="error-message">{errors.name}</span>}
      </div>
      
      <div className="form-group">
        <input 
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          placeholder="Din e-post"
          className={`form-input ${errors.email ? 'error' : ''}`}
        />
        {errors.email && <span className="error-message">{errors.email}</span>}
      </div>
      
      <button type="submit" className="submit-button">
        Skicka meddelande
      </button>
    </form>
  );
}

Lägg till CSS för fel:

.form-group {
  margin-bottom: 15px;
}

.form-input.error {
  border-color: #dc3545;
}

.error-message {
  color: #dc3545;
  font-size: 14px;
  display: block;
  margin-top: 5px;
}

Testa: Försök skicka utan att fylla i fälten, eller med ogiltig e-post

Steg 4: Fler formulärelement

Låt oss utöka med olika typer av input:

function CompleteForm() {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    age: '',
    country: '',
    newsletter: false,
    message: ''
  });

  const handleInputChange = (e) => {
    const { name, value, type, checked } = e.target;
    
    setFormData(prev => ({
      ...prev,
      [name]: type === 'checkbox' ? checked : value
    }));
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    console.log('Komplett formulär:', formData);
  };

  return (
    <form onSubmit={handleSubmit} className="complete-form">
      <h2>Registrera dig</h2>
      
      {/* Textfält */}
      <input 
        type="text"
        name="name"
        value={formData.name}
        onChange={handleInputChange}
        placeholder="Namn"
        className="form-input"
      />
      
      {/* E-post */}
      <input 
        type="email"
        name="email"
        value={formData.email}
        onChange={handleInputChange}
        placeholder="E-post"
        className="form-input"
      />
      
      {/* Nummer */}
      <input 
        type="number"
        name="age"
        value={formData.age}
        onChange={handleInputChange}
        placeholder="Ålder"
        className="form-input"
        min="13"
        max="120"
      />
      
      {/* Dropdown */}
      <select 
        name="country"
        value={formData.country}
        onChange={handleInputChange}
        className="form-input"
      >
        <option value="">Välj land</option>
        <option value="sweden">Sverige</option>
        <option value="norway">Norge</option>
        <option value="denmark">Danmark</option>
        <option value="finland">Finland</option>
      </select>
      
      {/* Checkbox */}
      <label className="checkbox-label">
        <input 
          type="checkbox"
          name="newsletter"
          checked={formData.newsletter}
          onChange={handleInputChange}
        />
        Jag vill få nyhetsbrev
      </label>
      
      {/* Textarea */}
      <textarea 
        name="message"
        value={formData.message}
        onChange={handleInputChange}
        placeholder="Meddelande (valfritt)"
        className="form-input"
        rows="4"
      />
      
      <button type="submit" className="submit-button">
        Registrera
      </button>
      
      {/* Debug: visa vad som fylls i */}
      <details style={{ marginTop: '20px' }}>
        <summary>Debug: Se formulärdata</summary>
        <pre>{JSON.stringify(formData, null, 2)}</pre>
      </details>
    </form>
  );
}

Lägg till CSS:

.checkbox-label {
  display: flex;
  align-items: center;
  margin: 10px 0;
  cursor: pointer;
}

.checkbox-label input {
  margin-right: 8px;
}

textarea.form-input {
  resize: vertical;
  min-height: 80px;
}

Steg 5: Experimentera själv! 🎯

Nu har du grunderna för React-formulär. Prova att lägga till:

Enkla förbättringar:

// Realtidsvalidering (validera medan användaren skriver)
const handleNameChange = (e) => {
  const value = e.target.value;
  setName(value);
  
  if (value.length < 2) {
    setNameError('Namnet måste vara minst 2 tecken');
  } else {
    setNameError('');
  }
};

// Räkna tecken i textarea
<div>
  <textarea /* ... */ />
  <small>{message.length}/500 tecken</small>
</div>

// Disable knapp tills formuläret är giltigt
<button 
  type="submit" 
  disabled={!name || !email || errors.name}
  className="submit-button"
>
  Skicka
</button>

Medelsvåra utmaningar:

  • Lösenordsstyrka: Visa färgad indikator för lösenordsstyrka
  • Bekräfta lösenord: Kontrollera att två lösenordsfält matchar
  • Fil-upload: Hantera filuppladdning med preview
  • Steg-för-steg formulär: Dela upp i flera steg/sidor

Avancerade idéer:

  • Auto-save: Spara formulärdata i localStorage medan användaren skriver
  • Async validering: Kontrollera om e-post redan finns (simulera API-anrop)
  • Dynamiska fält: Lägg till/ta bort fält baserat på användarens val

Populära formulärbibliotek

När dina formulär blir mer komplexa finns det hjälpsamma bibliotek:

  • React Hook Form: Prestanda-optimerat, minimal re-rendering
  • Formik: Populärt val med bra validering
  • React Final Form: Flexibelt och kraftfullt

Men lär dig grunderna först - förståelse för kontrollerade komponenter är grunden för allt formulärarbete i React!

Viktiga takeaways

Controlled components: React kontrollerar formulärvärdena via state
preventDefault(): Förhindra sidladdning vid submit
Validering: Ge användaren omedelbar feedback
Olika input-typer: text, email, number, select, checkbox, textarea
Användarvänlighet: Tydliga felmeddelanden och visuell feedback

Formulär är en av React:s största styrkor - du kan skapa riktigt interaktiva upplevelser! 🚀

Konsumera API:er i React: Data från Backend

Modern React-applikationer separerar frontend från backend och kommunicerar via API:er (Application Programming Interfaces). Detta kapitel fokuserar på hur vi hämtar, skickar och hanterar data från externa tjänster.

Mål: Lära sig använda Fetch API, skapa custom hooks för API-anrop, implementera robust error handling och förstå bästa praxis för datahantering. (Notis: Axios är ett populärt bibliotek om du vill ha extra funktioner.)

Fetch API: Webbstandardens Sätt

Fetch API är den moderna standarden för att göra HTTP-requests i JavaScript. Det är inbyggt i alla moderna webbläsare och behöver inga externa bibliotek.

Grundläggande Fetch-exempel

import { useState, useEffect } from 'react';

function UserList() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchUsers = async () => {
      try {
        setLoading(true);
        setError(null);

        const response = await fetch('/api/users');
        
        // Kontrollera om request lyckades
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }

        const userData = await response.json();
        setUsers(userData);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };

    fetchUsers();
  }, []);

  if (loading) return <div>Laddar användare...</div>;
  if (error) return <div>Fel: {error}</div>;

  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>
          {user.name} - {user.email}
        </li>
      ))}
    </ul>
  );
}

HTTP-metoder med Fetch

// GET (standard)
const getUsers = async () => {
  const response = await fetch('/api/users');
  return response.json();
};

// POST - Skapa ny användare
const createUser = async (userData) => {
  const response = await fetch('/api/users', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(userData),
  });

  if (!response.ok) {
    throw new Error('Failed to create user');
  }

  return response.json();
};

// PUT - Uppdatera användare
const updateUser = async (userId, userData) => {
  const response = await fetch(`/api/users/${userId}`, {
    method: 'PUT',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(userData),
  });

  return response.json();
};

// DELETE - Ta bort användare
const deleteUser = async (userId) => {
  const response = await fetch(`/api/users/${userId}`, {
    method: 'DELETE',
  });

  if (!response.ok) {
    throw new Error('Failed to delete user');
  }

  return response.ok;
};

Autentisering med Headers

// Token-baserad autentisering
const fetchWithAuth = async (url, options = {}) => {
  const token = localStorage.getItem('authToken');
  
  const config = {
    ...options,
    headers: {
      'Content-Type': 'application/json',
      ...(token && { Authorization: `Bearer ${token}` }),
      ...options.headers,
    },
  };

  const response = await fetch(url, config);

  // Hantera unauthorized
  if (response.status === 401) {
    localStorage.removeItem('authToken');
    // Navigera till login via central hantering (ex. router)
    throw new Error('Unauthorized');
  }

  return response;
};

// Användning
const getProtectedData = async () => {
  const response = await fetchWithAuth('/api/protected-data');
  return response.json();
};

Notis: Axios

Axios är ett populärt bibliotek för HTTP-anrop som erbjuder interceptors och några bekvämligheter. I denna kurs använder vi web standarden Fetch för alla exempel. Om du föredrar Axios kan du enkelt översätta våra fetch-anrop till axios.get/post/... och använda interceptors för t.ex. token-hantering.

Använd data från Pokemon API

Nu när vi har sett grunderna för hur man anropar ett API låt oss göra något roligare. Pokemon API är ett öppet API som ger oss tillgång till data om alla Pokemon från spelen - perfekt för att öva på API-anrop!

Vad är Pokemon API?

Pokemon API (PokeAPI) är ett RESTful API som innehåller information om:

  • Pokemon (namn, typ, statistik, bilder)
  • Moves (attacker)
  • Types (typer som Fire, Water, Electric)
  • Items (föremål)
  • Locations (platser)

Fördelar:

  • Helt gratis att använda
  • Ingen API-nyckel krävs
  • Välstrukturerad JSON-data
  • Stöder CORS (fungerar direkt från webbläsaren)

Undersök datan

Låt oss först utforska vad API:et ger oss. Öppna https://pokeapi.co/api/v2/pokemon/pikachu i webbläsaren eller testa med curl:

curl https://pokeapi.co/api/v2/pokemon/pikachu

Viktiga endpoints:

GET https://pokeapi.co/api/v2/pokemon/         # Lista första 20 Pokemon
GET https://pokeapi.co/api/v2/pokemon/{id}     # Specifik Pokemon (ID eller namn)
GET https://pokeapi.co/api/v2/type/{type}      # Pokemon av viss typ

Exempel på data för Pikachu:

{
  "id": 25,
  "name": "pikachu",
  "height": 4,
  "weight": 60,
  "types": [
    {
      "slot": 1,
      "type": {
        "name": "electric",
        "url": "https://pokeapi.co/api/v2/type/13/"
      }
    }
  ],
  "sprites": {
    "front_default": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/25.png",
    "front_shiny": "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/shiny/25.png"
  },
  "stats": [
    {
      "base_stat": 35,
      "stat": {
        "name": "hp"
      }
    }
    // ... fler stats
  ]
}

Bygg en Pokemon-app steg för steg

Låt oss börja enkelt och bygga upp funktionaliteten bit för bit.

Steg 1: Hämta och visa en Pokemon

Först - låt oss bara hämta Pikachu och visa namnet:

function PokemonApp() {
  const [pokemon, setPokemon] = useState(null);

  useEffect(() => {
    fetch('https://pokeapi.co/api/v2/pokemon/pikachu')
      .then(response => response.json())
      .then(data => setPokemon(data));
  }, []);

  return (
    <div className="pokemon-app">
      <h1>Min Pokemon App</h1>
      {pokemon && <h2>{pokemon.name}</h2>}
    </div>
  );
}

Testa detta först! Öppna Network-fliken i utvecklarverktygen och se API-anropet.

Steg 2: Lägg till bild och grundinfo

function PokemonApp() {
  const [pokemon, setPokemon] = useState(null);

  useEffect(() => {
    fetch('https://pokeapi.co/api/v2/pokemon/pikachu')
      .then(response => response.json())
      .then(data => setPokemon(data));
  }, []);

  return (
    <div className="pokemon-app">
      <h1>Min Pokemon App</h1>
      
      {pokemon && (
        <div className="pokemon-card">
          <h2>{pokemon.name}</h2>
          <img src={pokemon.sprites.front_default} alt={pokemon.name} />
          <p>Höjd: {pokemon.height / 10} m</p>
          <p>Vikt: {pokemon.weight / 10} kg</p>
        </div>
      )}
    </div>
  );
}

Steg 3: Lägg till sökfunktion

Nu gör vi det interaktivt:

function PokemonApp() {
  const [pokemon, setPokemon] = useState(null);
  const [searchTerm, setSearchTerm] = useState('');

  const searchPokemon = () => {
    fetch(`https://pokeapi.co/api/v2/pokemon/${searchTerm.toLowerCase()}`)
      .then(response => response.json())
      .then(data => setPokemon(data));
  };

  return (
    <div className="pokemon-app">
      <h1>Pokemon Sökare</h1>
      
      <div className="search-box">
        <input 
          type="text" 
          value={searchTerm}
          onChange={(e) => setSearchTerm(e.target.value)}
          placeholder="Skriv Pokemon namn..."
          className="search-input"
        />
        <button onClick={searchPokemon} className="search-button">
          Sök
        </button>
      </div>

      {pokemon && (
        <div className="pokemon-card">
          <h2>{pokemon.name}</h2>
          <img src={pokemon.sprites.front_default} alt={pokemon.name} />
          <p>Höjd: {pokemon.height / 10} m</p>
          <p>Vikt: {pokemon.weight / 10} kg</p>
          <p>Typ: {pokemon.types.map(type => type.type.name).join(', ')}</p>
        </div>
      )}
    </div>
  );
}

Prova att söka på: pikachu, charizard, bulbasaur, squirtle

Steg 4: Hantera fel och loading

Vad händer om vi söker på något som inte finns?

function PokemonApp() {
  const [pokemon, setPokemon] = useState(null);
  const [searchTerm, setSearchTerm] = useState('');
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  const searchPokemon = async () => {
    setLoading(true);
    setError(null);
    
    try {
      const response = await fetch(`https://pokeapi.co/api/v2/pokemon/${searchTerm.toLowerCase()}`);
      
      if (!response.ok) {
        throw new Error('Pokemon hittades inte!');
      }
      
      const data = await response.json();
      setPokemon(data);
    } catch (err) {
      setError(err.message);
      setPokemon(null);
    }
    
    setLoading(false);
  };

  return (
    <div className="pokemon-app">
      <h1>Pokemon Sökare</h1>
      
      <div className="search-box">
        <input 
          type="text" 
          value={searchTerm}
          onChange={(e) => setSearchTerm(e.target.value)}
          placeholder="Skriv Pokemon namn..."
          className="search-input"
        />
        <button 
          onClick={searchPokemon} 
          disabled={loading}
          className="search-button"
        >
          {loading ? 'Söker...' : 'Sök'}
        </button>
      </div>

      {error && <div className="error">Fel: {error}</div>}

      {pokemon && (
        <div className="pokemon-card">
          <h2>{pokemon.name}</h2>
          <img src={pokemon.sprites.front_default} alt={pokemon.name} />
          <p>Höjd: {pokemon.height / 10} m</p>
          <p>Vikt: {pokemon.weight / 10} kg</p>
          <p>Typ: {pokemon.types.map(type => type.type.name).join(', ')}</p>
        </div>
      )}
    </div>
  );
}

Lägg till CSS:

.search-box {
  margin: 20px 0;
}

.search-input {
  padding: 10px;
  margin-right: 10px;
  border: 1px solid #ddd;
  border-radius: 5px;
}

.search-button {
  padding: 10px 20px;
  background-color: #007bff;
  color: white;
  border: none;
  border-radius: 5px;
  cursor: pointer;
}

.search-button:disabled {
  background-color: #ccc;
  cursor: not-allowed;
}

.error {
  color: red;
  margin: 10px 0;
}

Testa fel: Sök på "asdfgh" och se vad som händer!

Steg 5: Gör appen din egen! 🎯

Nu har du en fungerande Pokemon-sökare. Dags att experimentera och lägga till egna funktioner!

Enkla förbättringar att prova:

Random Pokemon-knapp:

// Lägg till i din searchPokemon-funktion
const getRandomPokemon = () => {
  const randomId = Math.floor(Math.random() * 1010) + 1;
  setSearchTerm(randomId.toString());
  // Sedan kan du kalla searchPokemon() eller göra fetch direkt
};

// Lägg till knappen i din JSX
<button onClick={getRandomPokemon} className="random-button">
  Slumpa Pokemon
</button>

Visa shiny-versionen:

// I din pokemon-card, lägg till:
{pokemon.sprites.front_shiny && (
  <div>
    <p>Shiny version:</p>
    <img src={pokemon.sprites.front_shiny} alt={`Shiny ${pokemon.name}`} />
  </div>
)}

Visa Pokemon stats:

// Lägg till i pokemon-card:
<div className="stats">
  <h3>Stats:</h3>
  {pokemon.stats.map(stat => (
    <p key={stat.stat.name}>
      {stat.stat.name}: {stat.base_stat}
    </p>
  ))}
</div>

Medelsvåra utmaningar:

  • Sök med Enter: Gör så man kan trycka Enter i sökrutan
  • Favoriter: Spara favorit-Pokemon i localStorage
  • Historik: Visa de senaste sökta Pokemon
  • Jämför Pokemon: Visa två Pokemon sida vid sida

Avancerade idéer:

  • Pokemon Team Builder: Bygg ett lag med max 6 Pokemon
  • Type effectiveness: Visa vilka typer som är starka/svaga mot varandra
  • Evolution chain: Visa hela evolution-kedjan
  • Moves/attacker: Lista Pokemon:s attacker

Tips för utveckling:

// Enter-tangent för sökning
const handleKeyPress = (e) => {
  if (e.key === 'Enter') {
    searchPokemon();
  }
};

// Lägg till onKeyPress={handleKeyPress} på din input

// Spara i localStorage
const saveFavorite = () => {
  const favorites = JSON.parse(localStorage.getItem('favorites') || '[]');
  favorites.push(pokemon.name);
  localStorage.setItem('favorites', JSON.stringify(favorites));
};

Experimentera och ha kul! Det viktigaste är att du förstår hur API-anrop fungerar. Allt annat är bonus! 🎮

Custom Hooks för API-anrop

Custom hooks gör API‑logik återanvändbar och ren. För en komplett genomgång med många exempel (useApi, useResource, caching, retry m.m.), se fördjupningslektionen: Custom Hooks i React.

Glimåkra Webbutvecklare - Kursbok

Deployment med Dokploy: Från Kod till Live-webbplats

Modern webbutveckling handlar inte bara om att skriva kod - den måste också köras någonstans där användare kan komma åt den. Dokploy är en open source-plattform som gör det enkelt att deploya (publicera) dina React-applikationer på internet.

Mål: Lära sig att använda Dokploy för att deploya React-applikationer, förstå deployment-processen, och hantera domäner och SSL-certifikat.

Vad är Dokploy?

Dokploy är en self-hosted deployment-plattform som fungerar som ett alternativ till Netlify, Vercel och Heroku. Den låter dig:

  • 🚀 Deploya applikationer direkt från Git-repositories
  • 🌐 Hantera domäner och SSL-certifikat automatiskt
  • 📊 Övervaka dina applikationers prestanda
  • 🔧 Konfigurera miljövariabler och databaser

För denna kurs använder vi Dokploy-instansen på hoster.glimnet.se.

Förberedelser

1. Kontrollera din React-applikation

Innan deployment, se till att din React-app fungerar lokalt:

# Installera dependencies
npm install

# Testa att appen startar
npm run dev

# Testa att appen kan byggas för produktion
npm run build

2. Pusha kod till GitHub

Din kod måste finnas i ett GitHub-repository för att Dokploy ska kunna komma åt den:

# Initiera git (om inte redan gjort)
git init

# Lägg till alla filer
git add .

# Committa ändringarna
git commit -m "Initial commit"

# Lägg till remote repository
git remote add origin https://github.com/ditt-användarnamn/ditt-repo.git

# Pusha till GitHub
git push -u origin main

Steg 1: Skapa Dokploy-konto

Registrering

  1. Ge lärare din email
  2. Gå till din email inbox och klicka på inbjudan
  3. Gå till https://hoster.glimnet.se
  4. Fyll i dina uppgifter:
    • E-postadress
    • Lösenord
    • Bekräfta lösenord

Dokploy registrering

Första inloggningen

  1. Logga in med dina nya uppgifter
  2. Du kommer att se Dokploy dashboard

Dokploy Dashboard

Steg 2: Skapa ett nytt projekt

Projektinställningar

  1. Klicka på "New Project" eller "Nytt projekt"
  2. Ge projektet ett beskrivande namn (t.ex. "Min React App")
  3. Välj projekttyp: Application

Dialog Ruta

Steg 3: Skapa en ny Service

Projektinställningar

  1. Klicka på "New Service" eller "Ny Service"
  2. Ge projektet ett beskrivande namn (t.ex. "Min React App")
  3. Välj projekttyp: Application

Dialog Ruta

Anslut GitHub Repository

  1. Välj Git som källa
  2. Ange URL till ditt GitHub-repository:
    https://github.com/ditt-användarnamn/ditt-repo.git
    
  3. Välj branch (vanligtvis main eller master)
  4. Spara

Steg 3: Konfigurera Build-inställningar

Deployment Type

Dokploy kommer automatiskt att detektera att det är en React/Vite-applikation, men du kan behöva justera inställningarna:

Build Type: Välj Railpack (rekommenderat för React/Vite)

Avancerade inställningar (vid behov)

Om din applikation kräver specifika inställningar, kan du skapa konfigurationsfiler:

package.json - Se till att du har rätt scripts:

{
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "react": "^18.0.0",
    "react-dom": "^18.0.0"
  }
}

Steg 4: Domän och SSL-konfiguration

Konfigurera domän

  1. Gå till Domains-fliken i ditt projekt
  2. Klicka på "Add Domain"
  3. Fyll i domäninställningar:
    • Host: ditt-projektnamn.hoster.glimnet.se
    • Path: / (root)
    • Internal Path: / (viktigt!)
    • Container Port: 80

SSL-certifikat

  1. Aktivera HTTPS
  2. Välj Let's Encrypt som Certificate Provider
  3. Systemet kommer automatiskt att skaffa och installera SSL-certifikat

Steg 5: Deploya applikationen

Starta deployment

  1. Gå tillbaka till General-fliken
  2. Klicka på "Deploy"-knappen
  3. Vänta medan Dokploy:
    • Klonar din kod från GitHub
    • Installerar dependencies (npm install)
    • Bygger applikationen (npm run build)
    • Startar containern

Övervaka deployment

  1. Gå till Deployments-fliken för att se status
  2. Klicka på den senaste deploymenten för att se loggar
  3. Vänta tills status visar "✅ Done"

Steg 6: Testa din live-webbplats

Första testet

  1. När deploymenten är klar, klicka på domänlänken eller gå till:

    https://ditt-projektnamn.hoster.glimnet.se
    
  2. Din React-applikation ska nu vara live på internet! 🎉

Felsökning vid problem

Om webbplatsen inte fungerar, kontrollera:

  • 404-fel: Kontrollera att Internal Path är satt till /
  • 502 Bad Gateway: Kontrollera att Container Port är 80
  • Build-fel: Kontrollera deployment logs för felmeddelanden

Steg 7: Uppdatera applikationen

Automatisk deployment

Dokploy kan konfigureras för automatisk deployment vid kod-ändringar:

  1. Gå till SettingsGit
  2. Aktivera Auto Deploy
  3. Välj branch att övervaka (t.ex. main)

Nu kommer Dokploy automatiskt att deploya när du pushar ändringar till GitHub!

Manuell deployment

För manuell uppdatering:

  1. Pusha dina ändringar till GitHub:

    git add .
    git commit -m "Uppdatering av applikation"
    git push
    
  2. Gå till Dokploy dashboard

  3. Klicka "Deploy" igen

Avancerade funktioner

Miljövariabler

För att lägga till miljövariabler (t.ex. API-nycklar):

  1. Gå till Environment-fliken
  2. Lägg till variabler:
    VITE_API_URL=https://api.example.com
    VITE_APP_NAME=Min App
    

[PRINTSCREEN: Environment variables]

Custom domän

För att använda en subdomän på hoster.glimnet.se (t.ex. min-app.hoster.glimnet.se):

  1. Gå till "Domains"-fliken i Dokploy
  2. Klicka på "Add Domain"
  3. Ange önskad subdomän i formatet [namn].hoster.glimnet.se
  4. SSL-certifikat genereras automatiskt

Detta ger dig en säker HTTPS-anslutning utan extra konfiguration.

Monitoring och loggar

Övervaka din applikation:

  1. Logs-fliken: Se applikationsloggar
  2. Monitoring-fliken: Prestanda och resursanvändning
  3. Deployments-fliken: Historik över deployments

Vanliga problem och lösningar

Problem: "Build failed"

Orsak: Fel i koden eller missing dependencies

Lösning:

  1. Kontrollera deployment logs
  2. Testa npm run build lokalt
  3. Fixa eventuella fel och pusha igen

Problem: "404 Not Found"

Orsak: Fel Internal Path eller routing-problem

Lösning:

  1. Kontrollera att Internal Path är /
  2. Kontrollera att Container Port är 80
  3. För React Router: Lägg till try_files konfiguration

Problem: "502 Bad Gateway"

Orsak: Applikationen startar inte korrekt

Lösning:

  1. Kontrollera Container Port (ska vara 80 för Railpack)
  2. Kontrollera applikationsloggar
  3. Testa att appen fungerar lokalt

Säkerhet och best practices

1. Miljövariabler för känslig data

# ❌ Lägg aldrig känslig data direkt i koden
const API_KEY = "abc123secret";

# ✅ Använd miljövariabler
const API_KEY = import.meta.env.VITE_API_KEY;

2. HTTPS överallt

  • Använd alltid HTTPS för produktion
  • Dokploy hanterar SSL-certifikat automatiskt
  • Omdirigera HTTP till HTTPS

3. Regelbundna uppdateringar

  • Håll dependencies uppdaterade
  • Testa ändringar lokalt innan deployment
  • Använd versionshantering (Git tags) för viktiga releases

Sammanfattning

Deployment med Dokploy gör det enkelt att få din React-applikation live på internet:

  1. Förbered din kod och pusha till GitHub
  2. Skapa Dokploy-konto och projekt
  3. Konfigurera build-inställningar och domän
  4. Deploya och övervaka processen
  5. Testa din live-webbplats
  6. Uppdatera genom att pusha ändringar

Med Dokploy kan du fokusera på att utveckla fantastiska applikationer medan plattformen hanterar infrastrukturen åt dig!

Nästa steg

Nu när du kan deploya React-applikationer, utforska:

  • Databaser: Lägg till PostgreSQL eller MongoDB
  • API:er: Bygg och deploya backend-tjänster
  • Monitoring: Sätt upp aviseringar för driftstörningar
  • CI/CD: Automatisera testing och deployment

Grattis! Du har nu deployat din första React-applikation! 🚀


Behöver du hjälp? Kontakta lärare eller kolla in Dokploy dokumentation för mer avancerade funktioner.

Single Page Application: Koncept och struktur

En Single Page Application (SPA) laddar appen en gång och uppdaterar sedan innehållet i webbläsaren vid navigering. Här fokuserar vi på koncept, trade‑offs och enkel struktur.

SPA vs Traditionella Webbapplikationer

graph TB
    subgraph "Traditional Multi-Page App"
        A1[Browser] -->|"Request page1.html"| B1[Server]
        B1 -->|"Full HTML page"| A1
        A1 -->|"Request page2.html"| B1
        B1 -->|"Full HTML page"| A1
        A1 -->|"Request page3.html"| B1
        B1 -->|"Full HTML page"| A1
    end
    
    subgraph "Single Page Application"
        A2[Browser] -->|"Initial request"| B2[Server]
        B2 -->|"HTML + JS Bundle"| A2
        A2 -->|"API request"| B2
        B2 -->|"JSON data"| A2
        A2 -.->|"Client-side routing"| A2
    end
    
    style B1 fill:#ffa07a
    style A2 fill:#98fb98
    style B2 fill:#87ceeb

Fördelar med SPA (varför)

Användarupplevelse:

  • Snabbare navigering efter initial laddning
  • Smidigare övergångar mellan vyer
  • Möjlighet för offline-funktionalitet
  • App-liknande känsla

Utveckling:

  • Tydlig separation mellan frontend och backend
  • Återanvändbar API för flera klienter
  • Modern utvecklingsworkflow med hot reloading

Nackdelar med SPA (varför inte alltid)

Initial Prestanda:

  • Större initial JavaScript-bundle
  • Längre tid till första visning
  • Komplicerad state management

SEO och Tillgänglighet:

  • Kräver Server-Side Rendering (SSR) för SEO
  • Komplexare routing-implementation
  • Potential för minnesläckor

Enkel struktur för en liten SPA

Håll mappstrukturen enkel i början:

src/
  components/   # Återanvändbara UI-byggstenar
  pages/        # Sidor för routing
  services/     # API-anrop (fetch)
  hooks/        # Egen logik (valfritt)
  App.jsx       # Router och layout
  main.jsx      # Entrypoint

Layout-komponent med Navigation

// components/Layout.js
import { useState } from 'react';
import { Link, useLocation, useNavigate } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
import { useTheme } from '../contexts/ThemeContext';

function Layout({ children }) {
  const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
  const { user, logout } = useAuth();
  const { theme, toggleTheme } = useTheme();
  const location = useLocation();
  const navigate = useNavigate();

  const handleLogout = async () => {
    await logout();
    navigate('/');
    setMobileMenuOpen(false);
  };

  const isActive = (path) => location.pathname === path;

  return (
    <div className={`app ${theme}`}>
      <header className="header">
        <div className="container">
          <Link to="/" className="logo">
            <h1>Min SPA</h1>
          </Link>

          {/* Desktop Navigation */}
          <nav className="desktop-nav">
            <Link 
              to="/" 
              className={isActive('/') ? 'active' : ''}
            >
              Hem
            </Link>
            <Link 
              to="/products" 
              className={isActive('/products') ? 'active' : ''}
            >
              Produkter
            </Link>
            <Link 
              to="/about" 
              className={isActive('/about') ? 'active' : ''}
            >
              Om oss
            </Link>
          </nav>

          {/* User Actions */}
          <div className="user-actions">
            <button onClick={toggleTheme} className="theme-toggle">
              {theme === 'light' ? '🌙' : '☀️'}
            </button>

            {user ? (
              <div className="user-menu">
                <Link to="/dashboard">Dashboard</Link>
                <button onClick={handleLogout}>Logga ut</button>
              </div>
            ) : (
              <Link to="/login" className="login-btn">
                Logga in
              </Link>
            )}

            {/* Mobile Menu Toggle */}
            <button 
              className="mobile-menu-toggle"
              onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
            >
              ☰
            </button>
          </div>
        </div>

        {/* Mobile Navigation */}
        {mobileMenuOpen && (
          <nav className="mobile-nav">
            <Link to="/" onClick={() => setMobileMenuOpen(false)}>Hem</Link>
            <Link to="/products" onClick={() => setMobileMenuOpen(false)}>Produkter</Link>
            <Link to="/about" onClick={() => setMobileMenuOpen(false)}>Om oss</Link>
            {user && (
              <Link to="/dashboard" onClick={() => setMobileMenuOpen(false)}>
                Dashboard
              </Link>
            )}
          </nav>
        )}
      </header>

      <main className="main">
        {children}
      </main>

      <footer className="footer">
        <div className="container">
          <p>&copy; 2024 Min SPA. Alla rättigheter förbehållna.</p>
        </div>
      </footer>
    </div>
  );
}

export default Layout;

Context API för Global State

// contexts/AppContext.js - Kombinerad state management
import { createContext, useContext, useReducer, useEffect } from 'react';

const AppContext = createContext();

// Action types
const ACTIONS = {
  SET_LOADING: 'SET_LOADING',
  SET_ERROR: 'SET_ERROR',
  CLEAR_ERROR: 'CLEAR_ERROR',
  SET_USER: 'SET_USER',
  SET_PRODUCTS: 'SET_PRODUCTS',
  ADD_TO_CART: 'ADD_TO_CART',
  REMOVE_FROM_CART: 'REMOVE_FROM_CART',
  CLEAR_CART: 'CLEAR_CART',
  SET_THEME: 'SET_THEME'
};

// Initial state
const initialState = {
  loading: false,
  error: null,
  user: null,
  products: [],
  cart: [],
  theme: 'light'
};

// Reducer function
function appReducer(state, action) {
  switch (action.type) {
    case ACTIONS.SET_LOADING:
      return { ...state, loading: action.payload };
    
    case ACTIONS.SET_ERROR:
      return { ...state, error: action.payload, loading: false };
    
    case ACTIONS.CLEAR_ERROR:
      return { ...state, error: null };
    
    case ACTIONS.SET_USER:
      return { ...state, user: action.payload };
    
    case ACTIONS.SET_PRODUCTS:
      return { ...state, products: action.payload };
    
    case ACTIONS.ADD_TO_CART:
      const existingItem = state.cart.find(item => item.id === action.payload.id);
      if (existingItem) {
        return {
          ...state,
          cart: state.cart.map(item =>
            item.id === action.payload.id
              ? { ...item, quantity: item.quantity + 1 }
              : item
          )
        };
      }
      return {
        ...state,
        cart: [...state.cart, { ...action.payload, quantity: 1 }]
      };
    
    case ACTIONS.REMOVE_FROM_CART:
      return {
        ...state,
        cart: state.cart.filter(item => item.id !== action.payload)
      };
    
    case ACTIONS.CLEAR_CART:
      return { ...state, cart: [] };
    
    case ACTIONS.SET_THEME:
      return { ...state, theme: action.payload };
    
    default:
      return state;
  }
}

// Provider component
export function AppProvider({ children }) {
  const [state, dispatch] = useReducer(appReducer, initialState);

  // Load initial data
  useEffect(() => {
    const loadInitialData = async () => {
      try {
        dispatch({ type: ACTIONS.SET_LOADING, payload: true });
        
        // Load user from localStorage
        const savedUser = localStorage.getItem('user');
        if (savedUser) {
          dispatch({ type: ACTIONS.SET_USER, payload: JSON.parse(savedUser) });
        }

        // Load theme from localStorage
        const savedTheme = localStorage.getItem('theme') || 'light';
        dispatch({ type: ACTIONS.SET_THEME, payload: savedTheme });

        // Load products
        const response = await fetch('/api/products');
        const products = await response.json();
        dispatch({ type: ACTIONS.SET_PRODUCTS, payload: products });

      } catch (error) {
        dispatch({ type: ACTIONS.SET_ERROR, payload: error.message });
      } finally {
        dispatch({ type: ACTIONS.SET_LOADING, payload: false });
      }
    };

    loadInitialData();
  }, []);

  // Save user to localStorage when it changes
  useEffect(() => {
    if (state.user) {
      localStorage.setItem('user', JSON.stringify(state.user));
    } else {
      localStorage.removeItem('user');
    }
  }, [state.user]);

  // Save theme to localStorage when it changes
  useEffect(() => {
    localStorage.setItem('theme', state.theme);
    document.documentElement.setAttribute('data-theme', state.theme);
  }, [state.theme]);

  // Action creators
  const actions = {
    setLoading: (loading) => dispatch({ type: ACTIONS.SET_LOADING, payload: loading }),
    setError: (error) => dispatch({ type: ACTIONS.SET_ERROR, payload: error }),
    clearError: () => dispatch({ type: ACTIONS.CLEAR_ERROR }),
    setUser: (user) => dispatch({ type: ACTIONS.SET_USER, payload: user }),
    addToCart: (product) => dispatch({ type: ACTIONS.ADD_TO_CART, payload: product }),
    removeFromCart: (productId) => dispatch({ type: ACTIONS.REMOVE_FROM_CART, payload: productId }),
    clearCart: () => dispatch({ type: ACTIONS.CLEAR_CART }),
    toggleTheme: () => {
      const newTheme = state.theme === 'light' ? 'dark' : 'light';
      dispatch({ type: ACTIONS.SET_THEME, payload: newTheme });
    }
  };

  return (
    <AppContext.Provider value={{ state, actions }}>
      {children}
    </AppContext.Provider>
  );
}

// Custom hook
export function useApp() {
  const context = useContext(AppContext);
  if (!context) {
    throw new Error('useApp must be used within AppProvider');
  }
  return context;
}

Code Splitting och Lazy Loading

// Lazy load components
import { lazy, Suspense } from 'react';
import LoadingSpinner from './components/LoadingSpinner';

// Lazy loaded pages
const Dashboard = lazy(() => import('./pages/Dashboard'));
const AdminPanel = lazy(() => import('./pages/AdminPanel'));
const Reports = lazy(() => import('./pages/Reports'));

function App() {
  return (
    <Router>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route 
          path="/dashboard" 
          element={
            <Suspense fallback={<LoadingSpinner />}>
              <Dashboard />
            </Suspense>
          } 
        />
        <Route 
          path="/admin" 
          element={
            <Suspense fallback={<LoadingSpinner />}>
              <AdminPanel />
            </Suspense>
          } 
        />
        <Route 
          path="/reports" 
          element={
            <Suspense fallback={<LoadingSpinner />}>
              <Reports />
            </Suspense>
          } 
        />
      </Routes>
    </Router>
  );
}

Memoization och Optimization

import { memo, useMemo, useCallback } from 'react';

// Memoized list component
const ProductList = memo(function ProductList({ products, onAddToCart }) {
  const sortedProducts = useMemo(() => {
    // Undvik att mutera original-arrayen
    return [...products].sort((a, b) => a.name.localeCompare(b.name));
  }, [products]);

  return (
    <div className="product-grid">
      {sortedProducts.map(product => (
        <ProductCard
          key={product.id}
          product={product}
          onAddToCart={onAddToCart}
        />
      ))}
    </div>
  );
});

// Memoized product card
const ProductCard = memo(function ProductCard({ product, onAddToCart }) {
  const handleAddToCart = useCallback(() => {
    onAddToCart(product);
  }, [product, onAddToCart]);

  return (
    <div className="product-card">
      <img src={product.image} alt={product.name} loading="lazy" />
      <h3>{product.name}</h3>
      <p>{product.price} kr</p>
      <button onClick={handleAddToCart}>
        Lägg i kundvagn
      </button>
    </div>
  );
});

Virtual Scrolling för Stora Listor

import { FixedSizeList as List } from 'react-window';

function VirtualizedProductList({ products }) {
  const Row = ({ index, style }) => {
    const product = products[index];
    
    return (
      <div style={style} className="virtual-list-item">
        <ProductCard product={product} />
      </div>
    );
  };

  return (
    <List
      height={600}
      itemCount={products.length}
      itemSize={200}
      className="virtual-list"
    >
      {Row}
    </List>
  );
}

Build och deployment (översikt)

Optimering för Produktion

# Build för produktion
npm run build

# Analysera bundle size
npm install --save-dev webpack-bundle-analyzer
npx webpack-bundle-analyzer build/static/js/*.js

Miljövariabler

# .env.local
REACT_APP_API_URL=https://api.example.com
REACT_APP_GOOGLE_ANALYTICS_ID=GA_TRACKING_ID
REACT_APP_VERSION=$npm_package_version
// config/env.js
const config = {
  apiUrl: process.env.REACT_APP_API_URL || 'http://localhost:3001',
  googleAnalyticsId: process.env.REACT_APP_GOOGLE_ANALYTICS_ID,
  version: process.env.REACT_APP_VERSION || '1.0.0',
  isDevelopment: process.env.NODE_ENV === 'development',
  isProduction: process.env.NODE_ENV === 'production'
};

export default config;

Vite miljövariabler

# .env
VITE_API_URL=https://api.example.com
VITE_GOOGLE_ANALYTICS_ID=GA_TRACKING_ID
// src/config/env.js (Vite)
const config = {
  apiUrl: import.meta.env.VITE_API_URL || 'http://localhost:3001',
  googleAnalyticsId: import.meta.env.VITE_GOOGLE_ANALYTICS_ID,
  isDevelopment: import.meta.env.DEV,
  isProduction: import.meta.env.PROD
};

export default config;

När SSR/SSG?

  • SEO-kritiska sidor, snabb First Contentful Paint, delning av länkar med preview.
  • Överväg Next.js för Server-Side Rendering (SSR) eller Static Site Generation (SSG) när det passar behovet.
// public/sw.js
const CACHE_NAME = 'spa-cache-v1';
const urlsToCache = [
  '/',
  '/static/js/bundle.js',
  '/static/css/main.css',
  '/manifest.json'
];

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then((cache) => cache.addAll(urlsToCache))
  );
});

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request)
      .then((response) => {
        // Return cached version or fetch from network
        return response || fetch(event.request);
      })
  );
});

Deployment till olika plattformar

# Netlify
npm run build
# Dra och släpp build-mappen till Netlify

# Vercel
npm install -g vercel
vercel --prod

# GitHub Pages
npm install --save-dev gh-pages
# Lägg till i package.json:
# "homepage": "https://username.github.io/repository-name",
# "predeploy": "npm run build",
# "deploy": "gh-pages -d build"
npm run deploy

Mikro‑övning

Bygg en liten SPA med tre sidor (Hem, Om, Kontakt) och en sida som hämtar data (Produkter).

  • Navigera mellan sidor med React Router.
  • På Produkter: hämta en lista via Fetch och visa loading/error/tom‑state.
  • Lägg till en detaljsida med :id.

Klar‑kriterier:

  • Ingen sidladdning vid navigering.
  • Tydliga laddnings‑ och felmeddelanden.
  • Detaljsidan visar id från URL:en.

Error Tracking

// utils/errorTracking.js
class ErrorTracker {
  static init() {
    if (process.env.NODE_ENV === 'production') {
      window.addEventListener('error', this.handleError);
      window.addEventListener('unhandledrejection', this.handlePromiseRejection);
    }
  }

  static handleError(event) {
    console.error('Global error:', event.error);
    // Skicka till error tracking service
    this.sendToService({
      type: 'javascript_error',
      message: event.error.message,
      stack: event.error.stack,
      url: window.location.href,
      userAgent: navigator.userAgent
    });
  }

  static handlePromiseRejection(event) {
    console.error('Unhandled promise rejection:', event.reason);
    this.sendToService({
      type: 'promise_rejection',
      message: event.reason.toString(),
      url: window.location.href
    });
  }

  static sendToService(errorData) {
    fetch('/api/errors', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(errorData)
    }).catch(err => console.error('Failed to send error:', err));
  }
}

export default ErrorTracker;

Sammanfattning

Single Page Applications erbjuder många fördelar för moderna webbapplikationer:

  • Smidig användarupplevelse med snabb navigering
  • Tydlig separation mellan frontend och backend
  • Modern arkitektur som skalrar bra
  • Rich interaktivitet som desktop-applikationer

Viktiga aspekter att tänka på:

  • Performance optimization med code splitting
  • Proper state management för komplex data
  • Build-optimering för produktionsmiljö
  • Error tracking och monitoring
  • SEO-considerations för offentliga applikationer

I nästa avsnitt lär vi oss implementera routing med React Router för navigation.

Praktiska Övningar och Projekt: Bygg Kompletta React-applikationer

Nu när vi har lärt oss React-fundamenten är det dags att sätta samman allt i praktiska projekt. Dessa övningar bygger gradvis från enkla komponenter till kompletta applikationer.

Mål: Tillämpa React-kunskaper i verkliga projekt, integrera API:er, hantera state management och skapa användbara applikationer.

Övning 1: Todo-lista med useState

  • Bygg en enkel todo-app med möjligheter att lägga till, toggla och ta bort todos.
  • Visa antal kvarvarande todos och filtrering (alla/aktiva/klara).

Klar-kriterier:

  • Todo-listan överlever sidladdning.
  • Varje todo har stabil key (t.ex. id).
  • Inga muterande operationer på state-arrayer (använd spread operator ... ).

Övning 2: Konsumera publikt API + Routing

  • Hämta data från ett publikt API (t.ex. SWAPI eller PokéAPI).
  • Visa lista och detaljsida för ett item via React Router (v6), inkl. useParams.
  • Hantera loading/error-states och tomma resultat.

Klar-kriterier:

  • Lista och detaljvy fungerar via klient-routing (ingen helsidladdning).
  • Sök/filter uppdaterar URL query params med useSearchParams.

Övning 3: Formulär med validering och API

  • Bygg ett formulär (t.ex. registrering eller produktform) med kontrollerade inputs.
  • Validera fält vid blur och submit; visa felmeddelanden.
  • Skicka data till ett mock-API (json-server eller liknande) och hantera svar/fel.
  • Bonus: Använd react-hook-form för att jämföra ergonomi/prestanda.

Klar-kriterier:

  • Validering hindrar felaktiga submissioner och visar användarvänliga fel.
  • Efter lyckad submit nollställs formuläret eller navigera till en bekräftelsesida.

Övning 4: Global state med Context

  • Implementera tema-växling (ljus/mörk) med Context + persistens i localStorage.
  • Applicera temat på hela appen via CSS-klass eller data-themehtml-elementet.

Klar-kriterier:

  • Temat sparas och läses in vid start.
  • Ingen props drilling för temahantering.

Övning 5: Prestandaoptimering och stora listor

  • Rendera en lista med 1 000+ items.
  • Optimera med React.memo, useMemo, useCallback där det ger effekt.
  • Implementera virtualisering med react-window och jämför FPS/scroll-känsla.

Klar-kriterier:

  • Mätbar förbättring när virtualisering används.
  • Inga onödiga re-renders av listitems (verifiera med React DevTools Profiler).

Checklista (Definition of Done) för kapitel:

  • API-anrop hanterar loading/error och cleanup (AbortController).
  • Routing-exempel följer v6 (relativa paths, NavLink utan activeClassName).
  • Inga state-mutationer i exempel (kopior vid sortering/uppdatering).
  • ESLint react-hooks/exhaustive-deps aktiverat i projektet.
  • Tillgänglighet: formulär har etiketter, fokus är synligt, och knappar har beskrivande text.

Teknisk Intervju: Frontend-ramverk med React

Detta avsnitt innehåller vanliga tekniska intervjufrågor för React-utveckling. Frågorna täcker React fundamentals, komponenter, hooks, state management, routing och prestanda.

Fråga 1: React Grundläggande Koncept

Intervjuare: "Förklara vad React är och vad Virtual DOM innebär. Visa skillnaden mellan JSX och vanlig HTML."

Bra svar:

// React är ett JavaScript-bibliotek för att bygga användargränssnitt
// Virtual DOM är en representation av den riktiga DOM:en i minnet

// JSX - JavaScript XML (React syntax)
function Welcome({ name }) {
  return (
    <div className="greeting">
      <h1>Hej {name}!</h1>
      <p>Dagens datum: {new Date().toLocaleDateString()}</p>
    </div>
  );
}

// Motsvarande HTML skulle vara statisk:
// <div class="greeting">
//   <h1>Hej Anna!</h1>
//   <p>Dagens datum: 2024-01-15</p>
// </div>

// Skillnader JSX vs HTML:
// - className istället för class
// - {} för JavaScript-uttryck
// - camelCase för attribut (onClick, onChange)
// - Måste ha en parent element eller React Fragment

Förklaring: Virtual DOM gör att React kan jämföra förändringar effektivt och bara uppdatera de delar av DOM:en som ändrats. JSX tillåter JavaScript-uttryck inbäddade i markup.

Fråga 2: Funktionella vs Class Components

Intervjuare: "Vad är skillnaden mellan funktionella och class components? Varför föredras funktionella komponenter idag?"

Bra svar:

// Class Component (äldre sätt)
class ClassCounter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }

  componentDidMount() {
    console.log('Component mounted');
  }

  incrementCount = () => {
    this.setState({ count: this.state.count + 1 });
  }

  render() {
    return (
      <div>
        <p>Count: {this.state.count}</p>
        <button onClick={this.incrementCount}>Increment</button>
      </div>
    );
  }
}

// Functional Component med hooks (modernt sätt)
function FunctionalCounter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log('Component mounted/updated');
  }, []);

  const incrementCount = () => {
    setCount(count + 1);
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={incrementCount}>Increment</button>
    </div>
  );
}

Fördelar med funktionella komponenter:

  • Enklare syntax och mindre boilerplate-kod
  • Bättre prestanda med hooks
  • Lättare att testa och förstå
  • Modern React development standard

Fråga 3: useState och useEffect Hooks

Intervjuare: "Förklara hur useState och useEffect fungerar. Visa ett exempel med API-anrop."

Bra svar:

import { useState, useEffect } from 'react';

function UserProfile({ userId }) {
  // useState för state management
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  // useEffect för side effects (API calls, subscriptions etc)
  useEffect(() => {
    const fetchUser = async () => {
      try {
        setLoading(true);
        setError(null);
        
        const response = await fetch(`/api/users/${userId}`);
        if (!response.ok) {
          throw new Error('User not found');
        }
        
        const userData = await response.json();
        setUser(userData);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };

    fetchUser();
  }, [userId]); // Dependency array - kör när userId ändras

  // Cleanup effect (körs när component unmounts)
  useEffect(() => {
    return () => {
      console.log('Cleanup when component unmounts');
    };
  }, []);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  if (!user) return <div>No user found</div>;

  return (
    <div>
      <h2>{user.name}</h2>
      <p>Email: {user.email}</p>
    </div>
  );
}

useEffect dependency arrays:

  • [] - körs bara en gång (componentDidMount)
  • [userId] - körs när userId ändras
  • Ingen array - körs efter varje render

Fråga 4: Props och State Management

Intervjuare: "Förklara skillnaden mellan props och state. Visa hur man hanterar props drilling och Context API."

Bra svar:

// Props - data som skickas från parent till child
function Parent() {
  const [theme, setTheme] = useState('light');
  
  return (
    <Child 
      theme={theme} 
      onThemeChange={setTheme}
      title="My App"
    />
  );
}

function Child({ theme, onThemeChange, title }) {
  return (
    <div className={`app ${theme}`}>
      <h1>{title}</h1>
      <button onClick={() => onThemeChange(theme === 'light' ? 'dark' : 'light')}>
        Toggle Theme
      </button>
    </div>
  );
}

// Context API för att undvika props drilling
const ThemeContext = createContext();

function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  
  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}

// Använda context
function ThemedButton() {
  const { theme, setTheme } = useContext(ThemeContext);
  
  return (
    <button 
      className={`btn btn-${theme}`}
      onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}
    >
      Current theme: {theme}
    </button>
  );
}

// App med Context Provider
function App() {
  return (
    <ThemeProvider>
      <Header />
      <Main />
      <ThemedButton />
    </ThemeProvider>
  );
}

State vs Props:

  • State: Intern data som komponenten äger och kan ändra
  • Props: Data som skickas från föräldrakomponent, read-only

Fråga 5: Event Handling och Forms

Intervjuare: "Visa hur man hanterar formulär och events i React på ett kontrollerat sätt."

Bra svar:

function ContactForm() {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    message: '',
    subscribe: false
  });
  const [errors, setErrors] = useState({});
  const [isSubmitting, setIsSubmitting] = useState(false);

  // Hantera input changes
  const handleChange = (e) => {
    const { name, value, type, checked } = e.target;
    
    setFormData(prev => ({
      ...prev,
      [name]: type === 'checkbox' ? checked : value
    }));

    // Rensa fel när användaren börjar skriva
    if (errors[name]) {
      setErrors(prev => ({
        ...prev,
        [name]: ''
      }));
    }
  };

  // Validering
  const validateForm = () => {
    const newErrors = {};

    if (!formData.name.trim()) {
      newErrors.name = 'Namn är obligatoriskt';
    }

    if (!formData.email.trim()) {
      newErrors.email = 'E-post är obligatoriskt';
    } else if (!/\S+@\S+\.\S+/.test(formData.email)) {
      newErrors.email = 'Ogiltig e-postadress';
    }

    if (!formData.message.trim()) {
      newErrors.message = 'Meddelande är obligatoriskt';
    }

    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  };

  // Hantera form submission
  const handleSubmit = async (e) => {
    e.preventDefault();
    
    if (!validateForm()) return;

    setIsSubmitting(true);
    
    try {
      const response = await fetch('/api/contact', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(formData),
      });

      if (response.ok) {
        alert('Meddelande skickat!');
        setFormData({ name: '', email: '', message: '', subscribe: false });
      } else {
        throw new Error('Något gick fel');
      }
    } catch (error) {
      alert('Fel vid sändning: ' + error.message);
    } finally {
      setIsSubmitting(false);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="name">Namn:</label>
        <input
          type="text"
          id="name"
          name="name"
          value={formData.name}
          onChange={handleChange}
          className={errors.name ? 'error' : ''}
        />
        {errors.name && <span className="error-text">{errors.name}</span>}
      </div>

      <div>
        <label htmlFor="email">E-post:</label>
        <input
          type="email"
          id="email"
          name="email"
          value={formData.email}
          onChange={handleChange}
          className={errors.email ? 'error' : ''}
        />
        {errors.email && <span className="error-text">{errors.email}</span>}
      </div>

      <div>
        <label htmlFor="message">Meddelande:</label>
        <textarea
          id="message"
          name="message"
          value={formData.message}
          onChange={handleChange}
          className={errors.message ? 'error' : ''}
        />
        {errors.message && <span className="error-text">{errors.message}</span>}
      </div>

      <div>
        <label>
          <input
            type="checkbox"
            name="subscribe"
            checked={formData.subscribe}
            onChange={handleChange}
          />
          Prenumerera på nyhetsbrev
        </label>
      </div>

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Skickar...' : 'Skicka'}
      </button>
    </form>
  );
}

Viktiga principer för formulär i React:

  • Kontrollerade komponenter (controlled components)
  • Hantera all state i React
  • Validering både på input och submit
  • Förhindra default form behavior med preventDefault()

Fråga 6: Lists och Keys

Intervjuare: "Förklara varför keys är viktiga i React lists och visa best practices."

Bra svar:

function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: 'Handla mat', completed: false },
    { id: 2, text: 'Träna', completed: true },
    { id: 3, text: 'Studera React', completed: false }
  ]);

  const toggleTodo = (id) => {
    setTodos(todos.map(todo =>
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    ));
  };

  const deleteTodo = (id) => {
    setTodos(todos.filter(todo => todo.id !== id));
  };

  return (
    <ul>
      {todos.map(todo => (
        <TodoItem
          key={todo.id} // ✅ Använd stabil, unik key
          todo={todo}
          onToggle={toggleTodo}
          onDelete={deleteTodo}
        />
      ))}
    </ul>
  );
}

function TodoItem({ todo, onToggle, onDelete }) {
  return (
    <li className={todo.completed ? 'completed' : ''}>
      <span onClick={() => onToggle(todo.id)}>
        {todo.text}
      </span>
      <button onClick={() => onDelete(todo.id)}>
        Ta bort
      </button>
    </li>
  );
}

// ❌ Dåliga exempel på keys:
// key={index} - problem vid omordning eller radering
// key={Math.random()} - skapar nya keys vid varje render
// key={todo.text} - problem om text ändras eller är duplicerad

// ✅ Bra keys:
// key={todo.id} - stabil unik identifierare
// key={`${todo.userId}-${todo.timestamp}`} - sammansatt unik key

Varför keys är viktiga:

  • React använder keys för att identifiera vilka element som ändrats
  • Förbättrar prestanda vid re-rendering
  • Förhindrar buggar med component state

Fråga 7: React Router Navigation

Intervjuare: "Implementera routing med React Router inklusive protected routes."

Bra svar:

import { BrowserRouter, Routes, Route, Navigate, Link, useNavigate } from 'react-router-dom';

// Protected Route Component
function ProtectedRoute({ children }) {
  const isAuthenticated = useAuth(); // Custom hook för auth

  return isAuthenticated ? children : <Navigate to="/login" replace />;
}

// Navigation Component
function Navigation() {
  const { user, logout } = useAuth();
  const navigate = useNavigate();

  const handleLogout = () => {
    logout();
    navigate('/');
  };

  return (
    <nav>
      <Link to="/">Hem</Link>
      <Link to="/about">Om oss</Link>
      
      {user ? (
        <>
          <Link to="/dashboard">Dashboard</Link>
          <Link to="/profile">Profil</Link>
          <button onClick={handleLogout}>Logga ut</button>
        </>
      ) : (
        <>
          <Link to="/login">Logga in</Link>
          <Link to="/register">Registrera</Link>
        </>
      )}
    </nav>
  );
}

// Main App with Router
function App() {
  return (
    <AuthProvider>
      <BrowserRouter>
        <Navigation />
        
        <Routes>
          {/* Public routes */}
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
          <Route path="/login" element={<Login />} />
          <Route path="/register" element={<Register />} />
          
          {/* Protected routes */}
          <Route 
            path="/dashboard" 
            element={
              <ProtectedRoute>
                <Dashboard />
              </ProtectedRoute>
            } 
          />
          <Route 
            path="/profile" 
            element={
              <ProtectedRoute>
                <Profile />
              </ProtectedRoute>
            } 
          />
          
          {/* Dynamic routes */}
          <Route path="/users/:id" element={<UserProfile />} />
          
          {/* 404 */}
          <Route path="*" element={<NotFound />} />
        </Routes>
      </BrowserRouter>
    </AuthProvider>
  );
}

// Component med URL parameters
function UserProfile() {
  const { id } = useParams();
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetchUser(id).then(setUser);
  }, [id]);

  return user ? <div>{user.name}</div> : <div>Loading...</div>;
}

React Router hooks:

  • useNavigate() - programmatisk navigation
  • useParams() - hämta URL-parametrar
  • useLocation() - få information om current location

Fråga 8: Custom Hooks och API Integration

Intervjuare: "Skapa en custom hook för API-anrop med loading och error states."

Bra svar:

// Custom hook för API calls
function useApi(url, options = {}) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  const refetch = useCallback(async () => {
    try {
      setLoading(true);
      setError(null);
      
      const response = await fetch(url, {
        headers: {
          'Content-Type': 'application/json',
          ...options.headers,
        },
        ...options,
      });

      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }

      const result = await response.json();
      setData(result);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  }, [url, options]);

  useEffect(() => {
    refetch();
  }, [refetch]);

  return { data, loading, error, refetch };
}

// Custom hook för forms
function useForm(initialValues, validationRules = {}) {
  const [values, setValues] = useState(initialValues);
  const [errors, setErrors] = useState({});
  const [touched, setTouched] = useState({});

  const handleChange = (name, value) => {
    setValues(prev => ({ ...prev, [name]: value }));
    
    // Validera när användaren ändrar värde
    if (touched[name] && validationRules[name]) {
      const error = validationRules[name](value);
      setErrors(prev => ({ ...prev, [name]: error }));
    }
  };

  const handleBlur = (name) => {
    setTouched(prev => ({ ...prev, [name]: true }));
    
    // Validera när fält lämnas
    if (validationRules[name]) {
      const error = validationRules[name](values[name]);
      setErrors(prev => ({ ...prev, [name]: error }));
    }
  };

  const isValid = Object.values(errors).every(error => !error);

  return {
    values,
    errors,
    touched,
    handleChange,
    handleBlur,
    isValid,
    setValues,
    setErrors
  };
}

// Användning av custom hooks
function UsersList() {
  const { data: users, loading, error, refetch } = useApi('/api/users');
  
  const {
    values,
    errors,
    handleChange,
    handleBlur,
    isValid
  } = useForm(
    { name: '', email: '' },
    {
      name: (value) => !value ? 'Namn krävs' : '',
      email: (value) => !/\S+@\S+\.\S+/.test(value) ? 'Ogiltig e-post' : ''
    }
  );

  if (loading) return <div>Loading users...</div>;
  if (error) return <div>Error: {error}</div>;

  return (
    <div>
      <h2>Users</h2>
      <button onClick={refetch}>Refresh</button>
      
      <ul>
        {users?.map(user => (
          <li key={user.id}>{user.name} - {user.email}</li>
        ))}
      </ul>

      <form>
        <input
          type="text"
          placeholder="Name"
          value={values.name}
          onChange={(e) => handleChange('name', e.target.value)}
          onBlur={() => handleBlur('name')}
        />
        {errors.name && <span>{errors.name}</span>}
        
        <input
          type="email"
          placeholder="Email"
          value={values.email}
          onChange={(e) => handleChange('email', e.target.value)}
          onBlur={() => handleBlur('email')}
        />
        {errors.email && <span>{errors.email}</span>}
        
        <button disabled={!isValid}>Add User</button>
      </form>
    </div>
  );
}

Fördelar med custom hooks:

  • Återanvändbar logik mellan komponenter
  • Separation of concerns
  • Lättare testning
  • Renare komponentkod

Fråga 9: Performance Optimization

Intervjuare: "Vilka tekniker använder du för att optimera React-applikationer?"

Bra svar:

import { memo, useMemo, useCallback, lazy, Suspense } from 'react';

// 1. React.memo för att förhindra onödiga re-renders
const ExpensiveComponent = memo(function ExpensiveComponent({ data, onClick }) {
  console.log('ExpensiveComponent rendered');
  
  return (
    <div>
      <h3>{data.title}</h3>
      <button onClick={onClick}>Click me</button>
    </div>
  );
});

// 2. useMemo för dyra beräkningar
function ProductList({ products, searchTerm, sortBy }) {
  const filteredAndSortedProducts = useMemo(() => {
    console.log('Calculating filtered products...');
    
    let result = products.filter(product =>
      product.name.toLowerCase().includes(searchTerm.toLowerCase())
    );
    
    result.sort((a, b) => {
      if (sortBy === 'price') return a.price - b.price;
      if (sortBy === 'name') return a.name.localeCompare(b.name);
      return 0;
    });
    
    return result;
  }, [products, searchTerm, sortBy]); // Bara omberäkna när dependencies ändras

  return (
    <div>
      {filteredAndSortedProducts.map(product => (
        <ProductItem key={product.id} product={product} />
      ))}
    </div>
  );
}

// 3. useCallback för att stabilisera funktionsreferenser
function TodoApp() {
  const [todos, setTodos] = useState([]);
  
  // Utan useCallback skapas en ny funktion vid varje render
  const handleToggleTodo = useCallback((id) => {
    setTodos(prevTodos =>
      prevTodos.map(todo =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      )
    );
  }, []); // Tom dependency array eftersom vi använder functional update

  const handleDeleteTodo = useCallback((id) => {
    setTodos(prevTodos => prevTodos.filter(todo => todo.id !== id));
  }, []);

  return (
    <div>
      {todos.map(todo => (
        <TodoItem
          key={todo.id}
          todo={todo}
          onToggle={handleToggleTodo}
          onDelete={handleDeleteTodo}
        />
      ))}
    </div>
  );
}

// 4. Lazy loading med React.lazy
const LazyDashboard = lazy(() => import('./Dashboard'));
const LazyProfile = lazy(() => import('./Profile'));

function App() {
  return (
    <BrowserRouter>
      <Suspense fallback={<div>Loading...</div>}>
        <Routes>
          <Route path="/dashboard" element={<LazyDashboard />} />
          <Route path="/profile" element={<LazyProfile />} />
        </Routes>
      </Suspense>
    </BrowserRouter>
  );
}

// 5. Virtualisering för stora listor
import { FixedSizeList as List } from 'react-window';

function VirtualizedList({ items }) {
  const Row = ({ index, style }) => (
    <div style={style}>
      <div>{items[index].name}</div>
    </div>
  );

  return (
    <List
      height={600} // Höjd på container
      itemCount={items.length}
      itemSize={50} // Höjd per item
    >
      {Row}
    </List>
  );
}

Performance tips:

  • Använd React DevTools Profiler
  • Minimera re-renders med memo/useMemo/useCallback
  • Code splitting med lazy loading
  • Virtualisering för stora listor
  • Optimera bundle size
  • Använd production builds

Fråga 10: Error Boundaries och Error Handling

Intervjuare: "Implementera error boundaries och robustfehantering i React."

Bra svar:

// Error Boundary Class Component
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null, errorInfo: null };
  }

  static getDerivedStateFromError(error) {
    // Uppdatera state så nästa render visar fallback UI
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    // Logga felet till error reporting service
    console.error('Error caught by boundary:', error, errorInfo);
    
    this.setState({
      error: error,
      errorInfo: errorInfo
    });

    // Skicka till error monitoring (t.ex. Sentry)
    // errorReportingService.captureException(error);
  }

  render() {
    if (this.state.hasError) {
      return (
        <div className="error-boundary">
          <h2>Något gick fel</h2>
          <details style={{ whiteSpace: 'pre-wrap' }}>
            {this.state.error && this.state.error.toString()}
            <br />
            {this.state.errorInfo.componentStack}
          </details>
          <button onClick={() => window.location.reload()}>
            Ladda om sidan
          </button>
        </div>
      );
    }

    return this.props.children;
  }
}

// Custom hook för async error handling
function useAsyncError() {
  const [, setError] = useState();
  
  return useCallback((error) => {
    setError(() => {
      throw error;
    });
  }, []);
}

// Component med error handling
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const throwAsyncError = useAsyncError();

  useEffect(() => {
    const fetchUser = async () => {
      try {
        setLoading(true);
        const response = await fetch(`/api/users/${userId}`);
        
        if (!response.ok) {
          throw new Error(`Failed to fetch user: ${response.status}`);
        }
        
        const userData = await response.json();
        setUser(userData);
      } catch (error) {
        // Kasta error till Error Boundary
        throwAsyncError(error);
      } finally {
        setLoading(false);
      }
    };

    fetchUser();
  }, [userId, throwAsyncError]);

  if (loading) return <div>Loading...</div>;
  if (!user) return <div>User not found</div>;

  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
}

// App med Error Boundaries
function App() {
  return (
    <ErrorBoundary>
      <Router>
        <Header />
        <ErrorBoundary>
          <Routes>
            <Route path="/user/:id" element={<UserProfile />} />
            <Route path="/dashboard" element={
              <ErrorBoundary>
                <Dashboard />
              </ErrorBoundary>
            } />
          </Routes>
        </ErrorBoundary>
      </Router>
    </ErrorBoundary>
  );
}

Error handling best practices:

  • Använd Error Boundaries för att fånga rendering errors
  • Implementera graceful degradation
  • Logga fel till monitoring services
  • Visa användarvänliga felmeddelanden
  • Ha fallback UI för kritiska komponenter

Sammanfattning

Dessa frågor täcker de viktigaste aspekterna av React-utveckling:

  • React Fundamentals: JSX, Virtual DOM, komponenter
  • Hooks: useState, useEffect, custom hooks
  • State Management: Props, Context API, state lifting
  • Routing: React Router, navigation, protected routes
  • Performance: Memo, useMemo, useCallback, lazy loading
  • Error Handling: Error boundaries, async errors
  • Best Practices: Forms, API integration, testing

Förberedelse-tips:

  1. Bygg små React-projekt för att öva praktiskt
  2. Förstå React lifecycle och när olika hooks körs
  3. Öva på performance-optimering med React DevTools
  4. Lär dig skillnaden mellan controlled/uncontrolled components
  5. Förstå när och varför du skulle använda Context vs props

Fördjupning: React

Detta kapitel samlar vidare ämnen som blir aktuella när grunderna sitter. Syftet är att förklara när och varför du behöver dem, inte bara hur.

Varför ett separat fördjupningskapitel?

  • Hålla grunderna fokuserade: nybörjare lär sig snabbare med små, tydliga steg.
  • Kontext: avancerade mönster behövs först när appen växer (komplex routing, delad state, prestanda, SEO).
  • Beslutsstöd: förstå trade‑offs — när det är värt att öka komplexiteten.

Innehåll i fördjupningen:

  • Routing: skyddade sidor, dataladdning och felhantering
  • State: reducer + Context, normalisering och när tredjeparts‑store kan vara motiverad
  • Prestanda: memo, virtualisering, code‑splitting och när det inte behövs
  • Data: caching, retry/backoff, request‑dedupe och realtid (WebSockets)
  • SSR/SSG: när serverrendering är rätt val (Next.js)
  • TypeScript i React: varför och hur du inför det gradvis

Rekommenderad läsordning: hoppa till det problem du faktiskt har just nu.

Routing: skydd och data

Varför behövs detta?

  • Autentisering/auktorisering: vissa sidor ska bara vara tillgängliga för inloggade användare/roller.
  • Data‑först: ladda data innan en sida renderas, eller hantera fel centralt.
  • Bättre UX: behåll kontext, redirecta smart, och visa vänliga fel.

Skyddade routes (Protected routes)

När? När du har sidor som kräver inloggning eller specifik roll.

Grundidé: en wrapper som kontrollerar status och annars navigerar bort.

import { Navigate, useLocation } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';

export function ProtectedRoute({ children, requiredRole }) {
  const { user, loading } = useAuth();
  const location = useLocation();

  if (loading) return <div>Laddar…</div>;
  if (!user) return <Navigate to="/login" state={{ from: location }} replace />;
  if (requiredRole && user.role !== requiredRole) return <Navigate to="/unauthorized" replace />;

  return children;
}

Varför fungerar det? React Router v6 låter element vara vilken komponent som helst. Vi returnerar Navigate för att byta sida om villkor inte uppfylls.

Data‑laddning och fel (v6.4+ loaders)

När? När sidan behöver data innan den renderas (SEO/UX eller undvika blink).

Koncept: definiera en loader som hämtar data. Komponenten läser med useLoaderData().

import { createBrowserRouter, RouterProvider, useLoaderData } from 'react-router-dom';

async function productLoader({ params }) {
  const res = await fetch(`/api/products/${params.id}`);
  if (!res.ok) throw new Response('Not found', { status: 404 });
  return res.json();
}

function ProductDetail() {
  const product = useLoaderData();
  return <h1>{product.name}</h1>;
}

const router = createBrowserRouter([
  { path: '/products/:id', element: <ProductDetail />, loader: productLoader, errorElement: <div>Fel vid laddning</div> }
]);

export default function App() { return <RouterProvider router={router} />; }

Varför? Loader körs innan render — du får data/404 tidigt och kan visa rätt vy utan extra “loading” när data redan finns.

Felhantering för routes

Error boundaries per route fångar renderingsfel och visar fallback. Det förbättrar robusthet och UX.

När? I större appar eller när sidor har sköra beroenden.

Tips och trade‑offs

  • Skydd i klienten räcker inte för säkerhet — backend måste alltid verifiera.
  • Loaders förenklar dataflödet men låser in dig i Router‑pipen; använd där det passar.
  • Central 404/unauthorized förbättrar navigationsflödet.

State: reducer, Context och normalisering

Varför behövs detta?

  • När props‑drilling blir tungt och flera komponenter behöver samma data.
  • När state‑uppdateringar blir komplexa och kräver förutsägbarhet.

Reducer + Context

Reducer ger ett deterministiskt sätt att uppdatera state utifrån action → nytt state. Context delar detta state i trädet.

När? När flera delar av appen måste läsa/uppdatera samma state och “lyfta state” inte räcker.

import { createContext, useContext, useReducer } from 'react';

const AppContext = createContext();

function reducer(state, action) {
  switch (action.type) {
    case 'SET_USER':
      return { ...state, user: action.payload };
    default:
      return state;
  }
}

export function AppProvider({ children }) {
  const [state, dispatch] = useReducer(reducer, { user: null });
  const value = { state, dispatch };
  return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
}

export function useApp() {
  const ctx = useContext(AppContext);
  if (!ctx) throw new Error('useApp must be used within AppProvider');
  return ctx;
}

Varför? Reducer minskar implicit logik utspridd i många setState‑anrop och gör flöden testbara.

Normalisering av data

När? När du har inbäddade listor/objekt som blir svåra att uppdatera utan buggar.

Princip: spara data “platt” per typ (entities) och referera med ids.

Fördel: enklare uppdateringar och memoization; mindre risk för oavsiktliga re‑renders.

När extern state‑hanterare?

  • Du behöver features som devtools, middleware, time‑travel, asynk‑flöden.
  • Alternativ: Zustand, Redux Toolkit, Jotai. Välj när behoven finns — inte i förväg.

Custom Hooks i React

Inledning: Ett vanligt problem

Du bygger en lista över användare och en annan vy som visar senaste beställningar. I båda komponenterna skriver du samma kod: hämta data (fetch), visa loading, hantera error, och en knapp för att ladda om. Koden kopieras, divergerar och blir svår att underhålla.

// UsersPage.jsx
function UsersPage() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  const load = async () => {
    try {
      setLoading(true);
      setError(null);
      const res = await fetch('/api/users');
      setUsers(await res.json());
    } catch (e) {
      setError('Kunde inte hämta användare');
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => { load(); }, []);

  // ... renderar loading/error/lista
}

// OrdersPage.jsx (nästan identisk logik)
function OrdersPage() {
  const [orders, setOrders] = useState([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  const load = async () => {
    try {
      setLoading(true);
      setError(null);
      const res = await fetch('/api/orders');
      setOrders(await res.json());
    } catch (e) {
      setError('Kunde inte hämta beställningar');
    } finally {
      setLoading(false);
    }
  };// UsersPage.jsx
function UsersPage() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  const load = async () => {
    try {
      setLoading(true);
      setError(null);
      const res = await fetch('/api/users');
      setUsers(await res.json());
    } catch (e) {
      setError('Kunde inte hämta användare');
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => { load(); }, []);

  // ... renderar loading/error/lista
}

  useEffect(() => { load(); }, []);

  // ... renderar loading/error/lista
}

Problemet: samma mönster upprepas i flera komponenter. Varje liten ändring (t.ex. bättre felmeddelanden) måste göras överallt.

Lösningen: Skapa en custom hook

En custom hook (egen hook) kapslar in den upprepade logiken i en funktion som kan återanvändas.

// useResource.js
import { useEffect, useState } from 'react';

export function useResource(endpoint) {
  const [data, setData] = useState([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  const load = async () => {
    try {
      setLoading(true);
      setError(null);
      const res = await fetch(endpoint);
      setData(await res.json());
    } catch (e) {
      setError('Kunde inte hämta resurser');
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => { load(); }, []);

  return { data, loading, error, refetch: load };
}

// Användning i komponenter
function UsersPage() {
  const { data, loading, error, refetch } = useResource('/api/users');
  // ... enkel render, ingen duplicerad logik
}

function OrdersPage() {
  const { data, loading, error, refetch } = useResource('/api/orders');
  // ... enkel render, ingen duplicerad logik
}

Vinst: Komponenterna blir tunnare, logiken bor på ett ställe och kan förbättras centralt.

Varför Custom Hooks?

  • Återanvändning: dela logik mellan komponenter utan duplicering.
  • Separation: håll komponenter tunna; flytta komplexitet till hooks.
  • Testbarhet: testa logik isolerat från UI.
  • Inkapsling: göm implementation (fetch, timers, events) bakom enkel API.
  • Komposition: kombinera flera hooks till kraftfulla mönster.

Regler för Hooks

  • Anropa hooks högst upp i funktionen, inte i loopar/if/switch.
  • Anropa hooks endast från React‑funktioner (komponenter eller andra hooks).
  • Följ namngivningen useNamn så ESLint kan verifiera reglerna.

Bästa praxis

  • Namn: beskriv vad hooken gör (useDebounce, useLocalStorage).
  • API: returnera värden/funktioner i ett objekt för läsbarhet, eller en array när ordningen är viktig (som useState).
  • Stabilitet: använd useCallback/useMemo för att exponera stabila funktioner/objekt.
  • Fel: exponera error och loading när det är relevant (t.ex. data‑hooks).
  • Beroenden: ta in parametrar istället för att hårdkoda (endpoint, initial values, TTL etc.).

Exempel: Små, generella hooks

Enkla hooks löser vardagliga behov och gör din kod renare. Här är tre du ofta har nytta av — med varför de hjälper och hur de används.

useToggle — slå av/på ett boolean‑värde

Varför: perfekt för att öppna/stänga modaler, toggla dark mode eller visa/dölja element.

import { useState } from 'react';

export function useToggle(initial = false) {
  const [value, setValue] = useState(initial);
  const toggle = () => setValue(v => !v);
  return { value, toggle, setTrue: () => setValue(true), setFalse: () => setValue(false) };
}

// Användning
function Menu() {
  const { value: open, toggle } = useToggle();
  return (
    <div>
      <button onClick={toggle}>Meny</button>
      {open && <nav>…</nav>}
    </div>
  );
}

useCounter — enkel räknare

Varför: vanligen för pagination, mängd i varukorg eller steg i en wizard.

import { useState } from 'react';

export function useCounter(initial = 0) {
  const [count, setCount] = useState(initial);
  const inc = (step = 1) => setCount(c => c + step);
  const dec = (step = 1) => setCount(c => Math.max(0, c - step));
  const reset = () => setCount(initial);
  return { count, inc, dec, reset };
}

// Användning
function CartItem() {
  const { count, inc, dec } = useCounter(1);
  return (
    <div>
      <button onClick={() => dec()}>-</button>
      <span>{count}</span>
      <button onClick={() => inc()}>+</button>
    </div>
  );
}

useLocalStorage — spara enkel state över sidladdningar

Varför: behåll användarens val (tema, språk) mellan besök.

import { useState } from 'react';

export function useLocalStorage(key, defaultValue) {
  const [value, setValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : defaultValue;
    } catch {
      return defaultValue;
    }
  });

  const setStoredValue = (newValue) => {
    setValue(newValue);
    try { window.localStorage.setItem(key, JSON.stringify(newValue)); } catch {}
  };

  return [value, setStoredValue];
}

// Användning
function ThemeToggle() {
  const [theme, setTheme] = useLocalStorage('theme', 'light');
  return (
    <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
      Växla tema (nu: {theme})
    </button>
  );
}

Exempel: Data‑hooks för API

useApi

import { useState, useEffect, useCallback } from 'react';

export function useApi(apiFunction, dependencies = []) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  const execute = useCallback(async (...args) => {
    try {
      setLoading(true);
      setError(null);
      const result = await apiFunction(...args);
      setData(result);
      return result;
    } catch (err) {
      setError(err.message || 'Request failed');
      throw err;
    } finally {
      setLoading(false);
    }
  }, dependencies);

  useEffect(() => {
    execute();
  }, [execute]);

  const refetch = useCallback(() => execute(), [execute]);

  return { data, loading, error, refetch, execute };
}

// Användning
// const { data, loading, error, refetch } = useApi(() => fetch('/api/users').then(r => r.json()));

useResource (CRUD)

import { useState, useCallback, useEffect } from 'react';

export function useResource(endpoint) {
  const [data, setData] = useState([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  const fetchJson = async (url, options = {}) => {
    const res = await fetch(url, {
      headers: { 'Content-Type': 'application/json', ...(options.headers || {}) },
      ...options,
    });
    const text = await res.text();
    const json = text ? JSON.parse(text) : null;
    if (!res.ok) {
      throw new Error(json?.message || res.statusText || 'Request failed');
    }
    return json;
  };

  const fetchAll = useCallback(async () => {
    try {
      setLoading(true);
      setError(null);
      const result = await fetchJson(endpoint);
      setData(Array.isArray(result) ? result : (result?.data ?? []));
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  }, [endpoint]);

  const create = useCallback(async (newItem) => {
    try {
      const result = await fetchJson(endpoint, {
        method: 'POST',
        body: JSON.stringify(newItem)
      });
      setData(prev => [...prev, result]);
      return result;
    } catch (err) {
      setError(err.message);
      throw err;
    }
  }, [endpoint]);

  const update = useCallback(async (id, updatedItem) => {
    try {
      const result = await fetchJson(`${endpoint}/${id}`, {
        method: 'PUT',
        body: JSON.stringify(updatedItem)
      });
      setData(prev => prev.map(item => item.id === id ? result : item));
      return result;
    } catch (err) {
      setError(err.message);
      throw err;
    }
  }, [endpoint]);

  const remove = useCallback(async (id) => {
    try {
      await fetchJson(`${endpoint}/${id}`, { method: 'DELETE' });
      setData(prev => prev.filter(item => item.id !== id));
    } catch (err) {
      setError(err.message);
      throw err;
    }
  }, [endpoint]);

  useEffect(() => {
    fetchAll();
  }, [fetchAll]);

  return { data, loading, error, create, update, remove, refetch: fetchAll };
}

Caching i en hook

import { useEffect, useState } from 'react';

class ApiCache {
  constructor(ttl = 5 * 60 * 1000) { // 5 min
    this.cache = new Map();
    this.ttl = ttl;
  }
  set(key, data) { this.cache.set(key, { data, timestamp: Date.now() }); }
  get(key) {
    const item = this.cache.get(key);
    if (!item) return null;
    if (Date.now() - item.timestamp > this.ttl) { this.cache.delete(key); return null; }
    return item.data;
  }
  clear() { this.cache.clear(); }
}

const apiCache = new ApiCache();

export function useCachedApi(key, apiFunction, dependencies = []) {
  const [data, setData] = useState(() => apiCache.get(key));
  const [loading, setLoading] = useState(!data);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      const cached = apiCache.get(key);
      if (cached) { setData(cached); setLoading(false); return; }
      try {
        setLoading(true);
        setError(null);
        const result = await apiFunction();
        apiCache.set(key, result);
        setData(result);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };
    fetchData();
  }, [key, ...dependencies]);

  return { data, loading, error };
}

Retry med exponential backoff

export const retryWithBackoff = async (fn, maxRetries = 3, baseDelay = 1000) => {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      if (attempt === maxRetries) throw error;
      const delay = baseDelay * Math.pow(2, attempt - 1);
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
};

useWebSocket (realtid)

import { useEffect, useState } from 'react';

export function useWebSocket(url) {
  const [socket, setSocket] = useState(null);
  const [lastMessage, setLastMessage] = useState(null);
  const [connectionStatus, setConnectionStatus] = useState('Disconnected');

  useEffect(() => {
    const ws = new WebSocket(url);
    ws.onopen = () => { setConnectionStatus('Connected'); setSocket(ws); };
    ws.onmessage = (event) => setLastMessage(JSON.parse(event.data));
    ws.onclose = () => { setConnectionStatus('Disconnected'); setSocket(null); };
    ws.onerror = () => setConnectionStatus('Error');
    return () => ws.close();
  }, [url]);

  const sendMessage = (message) => {
    if (socket && socket.readyState === WebSocket.OPEN) {
      socket.send(JSON.stringify(message));
    }
  };

  return { lastMessage, connectionStatus, sendMessage };
}

Säkerhet och robusthet

  • Tokens i localStorage kan stjälas via XSS. För skyddade sessioner: httpOnly‑cookies + CSRF‑skydd (SameSite/CSRF‑token).
  • Rensa resurser i useEffect‑cleanup (event listeners, WebSocket, timers) för att undvika minnesläckor.
  • Visa tydliga felmeddelanden och ge möjlighet till retry.

Kompositionsexempel

function SearchUsers() {
  const [query, setQuery] = useState('');
  const debounced = useDebounce(query, 300);
  const { data, loading, error } = useCachedApi(
    `users:${debounced}`,
    () => fetch(`/api/users?q=${encodeURIComponent(debounced)}`).then(r => r.json()),
    [debounced]
  );

  return (
    <div>
      <input value={query} onChange={e => setQuery(e.target.value)} placeholder="Sök användare" />
      {loading && <p>Laddar…</p>}
      {error && <p>Fel: {error}</p>}
      <ul>{data?.map(u => <li key={u.id}>{u.name}</li>)}</ul>
    </div>
  );
}

Vanliga fallgropar

  • Oklara beroenden i useEffect leder till oväntade anrop. Håll API:et rent och stabilt.
  • Läckande timers/sockets om cleanup saknas.
  • För breda hooks som gör “allt”. Håll dem fokuserade.

Kort om testning

  • Isolera nätverk: mocka fetch/API‑funktioner.
  • Testa brancher: loading, success, error och retry.

Vidare läsning

Prestanda: memo, virtualisering och splitting

Varför behövs detta?

  • Stora listor eller tunga komponenter kan göra UI segt.
  • Onödiga re‑renders fördyrar upplevelsen.

Guiden: optimera när du ser ett problem, inte i förväg.

Undvik onödiga re‑renders

  • Dela upp stora komponenter i mindre.
  • Använd React.memo för rena presentionskomponenter.
  • Använd useMemo för tunga beräkningar och useCallback för stabila callbacks.

Fallgrop: överanvändning gör koden svårare att förstå och kan ge sämre prestanda.

Virtualisering av listor

När? Listor med hundratals/tusentals rader.

Bibliotek: react-window, react-virtualized.

Varför? Rendera endast synliga rader → drastiskt mindre DOM‑arbete.

Code‑splitting och lazy loading

När? Stora sidor/verktyg som sällan används.

React.lazy + Suspense för att ladda delar på begäran. Varför? Kortare initial laddning, bättre TTI.

Mät innan du optimerar

  • Använd React DevTools Profiler för att se re‑renders.
  • Inspektera bundle‑storlek och nätverk.
  • Optimera det som faktiskt är flaskhalsen.

Data: caching, retry och realtid

Varför behövs detta?

  • Nätverket är opålitligt: retry/backoff förbättrar robusthet.
  • Onödiga anrop kostar: deduplicering och caching sparar resurser.
  • Realtid: WebSockets för chatt, notiser, dashboards.

Enkel caching och deduplicering

Idé: spara svar i minne en kort tid och hindra parallella identiska anrop.

Trade‑off: cache måste invalideras — bra för läs‑tung data som sällan ändras.

Retry och exponential backoff

När? Transienta fel (nätverk/timeout). Inte för 4xx (klientfel).

Varför? Ökar chansen att lyckas utan att belasta servern.

WebSockets (realtid)

När? Live‑uppdateringar (chatt, ticker, notifieringar).

Varför? Push från servern i stället för att polla ofta.

Tips: återanslutning, hjärtslag och backoff krävs för robusthet.

SSR/SSG och Next.js

Varför behövs detta?

  • SEO och snabb första rendering: innehåll finns i HTML vid första svar.
  • Prestanda på långsamma enheter/nät: mindre JS behövs för att visa första innehållet.

Begrepp:

  • SSR (Server‑Side Rendering): HTML genereras per request.
  • SSG (Static Site Generation): HTML genereras vid build (snabbt + billigt att serva).

När välja vad?

  • SSR: sidor med personaliserat/inloggat innehåll.
  • SSG: publika, ofta lästa sidor som sällan ändras (blogg, produktsidor).

Next.js kortfattat:

  • Filbaserad routing, inbyggd SSR/SSG, data‑fetching på servern, bildoptimering m.m.
  • Börja när du har behov (SEO/prestanda) som motiverar lärokostnaden.

TypeScript i React

Varför TypeScript?

  • Fångar fel vid utveckling (fel props, felaktiga svar från API).
  • Bättre autocompletion och refaktoreringsstöd.
  • Tydligare kontrakt mellan komponenter.

Props och state

// Button.tsx
type ButtonProps = {
  text: string;
  onClick?: () => void;
  disabled?: boolean;
};

export function Button({ text, onClick, disabled = false }: ButtonProps) {
  return (
    <button disabled={disabled} onClick={onClick}>
      {text}
    </button>
  );
}

Varför? Komponentens API blir själv‑dokumenterande och säkert.

Event‑typer

function Form() {
  const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
  };

  const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    console.log(e.target.value);
  };

  return (
    <form onSubmit={onSubmit}>
      <input onChange={onChange} />
      <button type="submit">Skicka</button>
    </form>
  );
}

Hooks och generics

function useFetch<T>(url: string) {
  const [data, setData] = React.useState<T | null>(null);
  const [error, setError] = React.useState<string | null>(null);

  React.useEffect(() => {
    let active = true;
    fetch(url)
      .then((r) => (r.ok ? r.json() : Promise.reject(r.statusText)))
      .then((json) => active && setData(json as T))
      .catch((err) => active && setError(String(err)));
    return () => {
      active = false;
    };
  }, [url]);

  return { data, error };
}

Gradvis införande

  • Starta med att typa nya filer (.tsx) och centrala modeller (User, Product).
  • Aktivera striktare TS‑regler först när teamet är bekvämt.
  • Behåll interop med JS: du kan blanda .ts/.tsx och .js/.jsx i samma projekt.

Tailwind CSS – Kort beskrivning och installationsguide

Vad är Tailwind CSS?

Tailwind CSS är ett modernt utility-first CSS-ramverk som revolutionerar hur vi skriver CSS. Istället för traditionell CSS där vi skapar anpassade klasser, använder Tailwind färdiga utility-klasser direkt i HTML/JSX. Detta innebär att du kan styla dina element med klasser som:

  • flex för flexbox-layout
  • mt-4 för margin-top
  • text-center för text-centrering
  • bg-blue-500 för bakgrundsfärg
  • hover:scale-105 för hover-effekter

Varför använda Tailwind CSS?

  • 🚀 Snabbare utveckling

    • Skriv styling direkt i HTML/JSX utan att växla mellan filer
    • Slipp hitta på klassnamn och återanvända CSS
    • Få direkt visuell feedback när du arbetar
  • 🎨 Konsekvent design

    • Fördefinierat design-system med färger, spacing och typografi
    • Enkelt att hålla en konsekvent look genom hela projektet
    • Anpassningsbart via tailwind.config.js
  • 📱 Effektiv responsiv design

    • Använd breakpoints som sm:, md:, lg: direkt i klasserna
    • Snabb och intuitiv mobilanpassning
    • Inga mediaqueries behövs
  • 🛠️ Optimerat för produktion

    • Automatisk eliminering av oanvänd CSS
    • Mindre filstorlek i produktion
    • Bättre prestanda
  • 🌍 Stark community & ekosystem

    • Omfattande dokumentation
    • Färdiga komponentbibliotek
    • Stort urval av plugins och verktyg

Installationsguide

1. Skapa nytt projekt

Om du inte redan har ett projekt:

npm create vite@latest my-project
cd my-project

2. Installera Tailwind CSS

Installera Tailwind och dess peer-dependencies:

npm install tailwindcss @tailwindcss/vite

3. Konfigurera Tailwind

Skapa eller uppdatera tailwind.config.js (Viktigt att den filen ligger i root-folder av projektet):

import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
  plugins: [
    tailwindcss(),
  ],
})

4. Lägg till Tailwind i CSS

Skapa eller uppdatera din huvudsakliga CSS-fil (t.ex. src/index.css). Om det redan ligger massa CSS där kommentera ut den eller ta bort.:

@import "tailwindcss";

5. Uppdatera HTML/JSX

Nu kan du börja använda Tailwind i dina komponenter. Exempel:

<div class="flex min-h-screen items-center justify-center bg-gray-100">
  <div class="rounded-lg bg-white p-8 shadow-lg">
    <h1 class="text-2xl font-bold text-gray-800">Hello Tailwind!</h1>
    <p class="mt-2 text-gray-600">Welcome to your new project</p>
  </div>
</div>

6. Starta utvecklingsservern

npm run dev

Verifiering

För att verifiera att Tailwind fungerar, kontrollera att:

  1. Utility-klasserna fungerar i webbläsaren
  2. Du ser Tailwind-specifik syntax-highlighting i din editor
  3. Du får autocompletion för Tailwind-klasser (installera relevant editor-tillägg om det behövs)

Kicka igång tailwind om utvecklingsservern varit avstängd

  1. Öppna en terminal
  2. Navigera till rätt folder
  3. Skriv in kommandot:
npm run dev

Anpassa Tailwind och lägga till ett eget tema

Utöka standardtemat

Du kan lägga till egna färger, typsnitt eller andra designparametrar genom att uppdatera tailwind.config.js. Här är ett exempel på hur du lägger till egna färger och typsnitt:

// filepath: tailwind.config.js
/** @type {import('tailwindcss').Config} */
export default {
  content: [
    "./index.html",
    "./src/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {
      colors: {
        brand: {
          light: '#3AB0FF',
          DEFAULT: '#007BFF',
          dark: '#0056B3',
        },
      },
      fontFamily: {
        custom: ['"Open Sans"', 'sans-serif'],
      },
    },
  },
  plugins: [],
}

Använd dina anpassningar

När du har lagt till dina egna färger och typsnitt kan du använda dem direkt i dina komponenter:

<div class="p-4 bg-brand text-white font-custom">
  Detta är en komponent med anpassade färger och typsnitt.
</div>

Lägg till fler anpassningar

Du kan också utöka andra delar av temat, som spacing, border-radius eller skuggor:

extend: {
  spacing: {
    '72': '18rem',
    '84': '21rem',
    '96': '24rem',
  },
  borderRadius: {
    'xl': '1.5rem',
  },
  boxShadow: {
    'custom': '0 4px 6px rgba(0, 0, 0, 0.1)',
  },
}

Tips för VS Code

Installera dessa tillägg för bättre Tailwind-stöd:

  • Tailwind CSS IntelliSense
  • PostCSS Language Support

Dokumentation

Tailwind CSS

Layout med Flexbox och Grid

📖 Flexbox i Tailwind

Flexbox används för att styra layout horisontellt och vertikalt.

Exempel 1 – Enkel navbar

<div class="flex justify-between items-center p-4 bg-gray-200">
  <div class="font-bold text-xl">Logo</div>
  <div class="space-x-4">
    <a href="#" class="hover:text-blue-600">Hem</a>
    <a href="#" class="hover:text-blue-600">Om oss</a>
    <a href="#" class="hover:text-blue-600">Kontakt</a>
  </div>
</div>

Förklaringar:

  • flex → Aktiverar flexbox.
  • justify-between → Sprider ut elementen horisontellt.
  • items-center → Centrerar vertikalt.
  • space-x-4 → Skapar horisontellt mellanrum mellan barn-element.

Exempel 2 – Centerad container

<div class="flex justify-center items-center h-64 bg-gray-100">
  <p class="text-lg">Centrerad text</p>
</div>

📖 Grid

Grid används för mer avancerade layouter med kolumner och rader.

<div class="grid grid-cols-3 gap-4 p-4">
  <div class="bg-red-300 p-4">1</div>
  <div class="bg-green-300 p-4">2</div>
  <div class="bg-blue-300 p-4">3</div>
</div>
  • grid-cols-3 → Tre kolumner.
  • gap-4 → Mellanrum mellan rader och kolumner.

📝 Övning

  1. Skapa en flexbox navbar med 5 länkar, centrera vertikalt.
  2. Bygg en 4-kolumns grid-layout med olika färger.
  3. Testa flex-col och flex-wrap för responsiv layout.

Tailwind CSS – Färger och Typografi

Introduktion

Tailwind CSS erbjuder ett kraftfullt och flexibelt sätt att hantera färger och typografi i dina projekt. Med hjälp av utility-klasser kan du snabbt och enkelt skapa en konsekvent design utan att behöva skriva anpassad CSS.


Färger i Tailwind CSS

Fördefinierade färger

Tailwind har ett stort urval av fördefinierade färger som är enkla att använda. Färgerna är organiserade i nyanser från 50 (ljusast) till 900 (mörkast).

Exempel på färger

<div class="p-4 bg-blue-500 text-white">Blå bakgrund</div>
<div class="p-4 bg-red-300 text-red-900">Röd bakgrund med mörk text</div>
<div class="p-4 bg-gray-100 text-gray-800">Ljusgrå bakgrund med mörkgrå text</div>

Anpassa färger

Du kan anpassa färger i tailwind.config.js:

// filepath: tailwind.config.js
module.exports = {
  theme: {
    extend: {
      colors: {
        brand: {
          light: '#3AB0FF',
          DEFAULT: '#007BFF',
          dark: '#0056B3',
        },
      },
    },
  },
};

Använd sedan dina anpassade färger:

<div class="p-4 bg-brand text-white">Anpassad färg</div>

Typografi i Tailwind CSS

Textstorlekar

Tailwind erbjuder fördefinierade textstorlekar som är enkla att använda:

<p class="text-sm">Liten text</p>
<p class="text-lg">Stor text</p>
<p class="text-2xl font-bold">Extra stor och fet text</p>

Typsnitt

Du kan använda fördefinierade typsnitt eller lägga till egna:

<p class="font-sans">Sans-serif typsnitt</p>
<p class="font-serif">Serif typsnitt</p>
<p class="font-mono">Monospace typsnitt</p>

Anpassa typsnitt

Lägg till egna typsnitt i tailwind.config.js:

// filepath: tailwind.config.js
module.exports = {
  theme: {
    extend: {
      fontFamily: {
        custom: ['"Open Sans"', 'sans-serif'],
      },
    },
  },
};

Använd sedan det anpassade typsnittet:

<p class="font-custom">Text med anpassat typsnitt</p>

Exempel: Kombinera färger och typografi

<div class="p-6 bg-gray-100 text-gray-800">
  <h1 class="text-3xl font-bold text-blue-600">Välkommen till Tailwind CSS</h1>
  <p class="mt-2 text-lg text-gray-700">
    Tailwind gör det enkelt att skapa snygga och responsiva gränssnitt.
  </p>
  <button class="mt-4 px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600">
    Läs mer
  </button>
</div>

Tips för responsiv design

Tailwind gör det enkelt att anpassa färger och typografi för olika skärmstorlekar:

<p class="text-sm sm:text-base md:text-lg lg:text-xl">
  Textstorleken ändras beroende på skärmstorlek.
</p>
<div class="bg-red-500 sm:bg-yellow-500 md:bg-green-500 lg:bg-blue-500">
  Bakgrundsfärgen ändras beroende på skärmstorlek.
</div>

Övning: Skapa en profilkortskomponent

Uppgift

Skapa ett responsivt profilkort med hjälp av Tailwind CSS. Kortet ska innehålla:

  1. En profilbild.
  2. Ett namn och en titel.
  3. En kort beskrivning.
  4. En knapp för att följa användaren.

Krav

  • Använd Tailwinds färg- och typografiklasser.
  • Gör kortet responsivt så att det ser bra ut på både mobil och desktop.

Exempel på HTML-struktur

<div class="max-w-sm mx-auto bg-white rounded-lg shadow-md overflow-hidden">
  <img class="w-full h-48 object-cover" src="https://via.placeholder.com/150" alt="Profilbild">
  <div class="p-4">
    <h2 class="text-xl font-bold text-gray-800">John Doe</h2>
    <p class="text-sm text-gray-600">Webbutvecklare</p>
    <p class="mt-2 text-gray-700">
      Passionerad utvecklare med erfarenhet av moderna ramverk som Tailwind CSS.
    </p>
    <button class="mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
      Följ
    </button>
  </div>
</div>

Utmaning

  • Lägg till en hover-effekt på kortet (t.ex. en skugga eller ändrad bakgrundsfärg).
  • Gör kortet klickbart genom att lägga till en länk runt hela kortet.

Sammanfattning

  • Färger: Använd fördefinierade eller anpassade färger för att skapa en konsekvent design.
  • Typografi: Hantera textstorlekar, typsnitt och andra textrelaterade egenskaper med utility-klasser.
  • Responsivitet: Anpassa färger och typografi för olika skärmstorlekar med Tailwinds breakpoints.

För mer information, besök Tailwind CSS Dokumentation.

Tailwind CSS – Responsiv Design

Introduktion

Responsiv design är en viktig del av modern webbutveckling. Med Tailwind CSS är det enkelt att skapa responsiva gränssnitt genom att använda dess inbyggda breakpoints och utility-klasser. Du kan snabbt anpassa layout, färger, typografi och andra stilar för olika skärmstorlekar utan att skriva egna media queries.


Breakpoints i Tailwind CSS

Tailwind använder följande standard-breakpoints:

BreakpointPrefixBeskrivning
smsm:För skärmar ≥ 640px
mdmd:För skärmar ≥ 768px
lglg:För skärmar ≥ 1024px
xlxl:För skärmar ≥ 1280px
2xl2xl:För skärmar ≥ 1536px

Du kan använda dessa prefix för att applicera olika stilar beroende på skärmstorlek.


Exempel: Responsiv layout

Här är ett exempel på hur du kan skapa en responsiv layout med Tailwind:

<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
  <div class="p-4 bg-blue-500 text-white">Kolumn 1</div>
  <div class="p-4 bg-green-500 text-white">Kolumn 2</div>
  <div class="p-4 bg-red-500 text-white">Kolumn 3</div>
</div>

Förklaring:

  • grid grid-cols-1: Skapar en grid-layout med en kolumn som standard.
  • sm:grid-cols-2: Vid skärmar ≥ 640px visas två kolumner.
  • lg:grid-cols-3: Vid skärmar ≥ 1024px visas tre kolumner.
  • gap-4: Lägger till mellanrum mellan kolumnerna.

Exempel: Responsiv typografi

Du kan också anpassa textstorlekar för olika skärmstorlekar:

<h1 class="text-2xl sm:text-3xl md:text-4xl lg:text-5xl">
  Responsiv rubrik
</h1>

Förklaring:

  • text-2xl: Standardstorlek för rubriken.
  • sm:text-3xl: Vid skärmar ≥ 640px ökas textstorleken.
  • md:text-4xl: Vid skärmar ≥ 768px ökas textstorleken ytterligare.
  • lg:text-5xl: Vid skärmar ≥ 1024px används den största textstorleken.

Exempel: Responsiv navigationsmeny

Här är ett exempel på en navigationsmeny som ändras mellan mobil och desktop:

<nav class="bg-gray-800 text-white p-4">
  <ul class="flex flex-col sm:flex-row sm:space-x-4">
    <li><a href="#" class="block py-2 sm:py-0">Hem</a></li>
    <li><a href="#" class="block py-2 sm:py-0">Om oss</a></li>
    <li><a href="#" class="block py-2 sm:py-0">Tjänster</a></li>
    <li><a href="#" class="block py-2 sm:py-0">Kontakt</a></li>
  </ul>
</nav>

Förklaring:

  • flex flex-col: Menyn visas som en vertikal lista som standard.
  • sm:flex-row: Vid skärmar ≥ 640px ändras menyn till en horisontell lista.
  • sm:space-x-4: Lägger till horisontellt mellanrum mellan menyalternativen på större skärmar.

Övning: Skapa en responsiv produktkortskomponent

Uppgift

Skapa en responsiv produktkortskomponent som innehåller:

  1. En bild av produkten.
  2. Produktens namn och pris.
  3. En kort beskrivning.
  4. En knapp för att lägga till produkten i varukorgen.

Krav

  • Kortet ska visas i en kolumn på små skärmar och i en rad på större skärmar.
  • Använd Tailwinds breakpoints för att göra kortet responsivt.
  • Lägg till en hover-effekt på knappen.

Exempel på HTML-struktur

<div class="max-w-md mx-auto bg-white rounded-lg shadow-md overflow-hidden sm:flex sm:items-center sm:space-x-4">
  <img class="w-full h-48 object-cover sm:w-32 sm:h-32" src="https://via.placeholder.com/150" alt="Produktbild">
  <div class="p-4">
    <h2 class="text-lg font-bold text-gray-800">Produktnamn</h2>
    <p class="text-gray-600">Kort beskrivning av produkten.</p>
    <p class="mt-2 text-green-500 font-semibold">Pris: 299 kr</p>
    <button class="mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
      Lägg till i varukorgen
    </button>
  </div>
</div>

Utmaning

  • Gör kortet klickbart genom att lägga till en länk runt hela kortet.
  • Lägg till en responsiv grid-layout som visar flera produktkort bredvid varandra på större skärmar.

Sammanfattning

  • Breakpoints: Använd Tailwinds inbyggda breakpoints för att skapa responsiva layouter.
  • Responsivitet: Anpassa layout, typografi och andra stilar för olika skärmstorlekar.
  • Praktisk tillämpning: Skapa komponenter som fungerar bra på både små och stora skärmar.

För mer information, besök Tailwind CSS Dokumentation.

Tailwind CSS – States och Interaktivitet

Introduktion

Tailwind CSS gör det enkelt att hantera olika tillstånd (states) och interaktivitet i dina komponenter. Med hjälp av pseudo-klassprefix som hover:, focus:, och active: kan du skapa dynamiska och responsiva användarupplevelser utan att behöva skriva anpassad CSS.


Vanliga pseudo-klasser i Tailwind CSS

Pseudo-klassPrefixBeskrivning
hoverhover:När användaren hovrar över ett element
focusfocus:När ett element är i fokus
activeactive:När ett element är aktivt (t.ex. klickat)
disableddisabled:När ett element är inaktiverat
group-hovergroup-hover:När en grupp är i hover-tillstånd

Exempel: Hover-effekter

Med hover: kan du enkelt skapa effekter när användaren hovrar över ett element.

<button class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
  Hovera över mig
</button>

Förklaring:

  • hover:bg-blue-600: Ändrar bakgrundsfärgen när användaren hovrar över knappen.

Exempel: Focus-effekter

Med focus: kan du styla element när de är i fokus, till exempel när en användare klickar på ett formulärfält.

<input class="border-2 border-gray-300 focus:border-blue-500 focus:outline-none p-2" type="text" placeholder="Skriv något...">

Förklaring:

  • focus:border-blue-500: Ändrar kantfärgen när fältet är i fokus.
  • focus:outline-none: Tar bort standardfokusramen.

Exempel: Active-effekter

Med active: kan du styla element när de är aktiva, till exempel när en knapp trycks ned.

<button class="px-4 py-2 bg-green-500 text-white rounded active:bg-green-700">
  Klicka på mig
</button>

Förklaring:

  • active:bg-green-700: Ändrar bakgrundsfärgen när knappen trycks ned.

Exempel: Disabled-tillstånd

Med disabled: kan du styla element som är inaktiverade.

<button class="px-4 py-2 bg-gray-400 text-white rounded disabled:opacity-50" disabled>
  Inaktiverad knapp
</button>

Förklaring:

  • disabled:opacity-50: Sänker opaciteten för inaktiverade element.

Exempel: Gruppbaserade tillstånd

Med group-hover: kan du skapa effekter som påverkar andra element i samma grupp.

<div class="group p-4 bg-gray-100 rounded-lg">
  <h2 class="text-lg font-bold group-hover:text-blue-500">Rubrik</h2>
  <p class="text-gray-600 group-hover:text-gray-800">Text som ändras vid hover.</p>
</div>

Förklaring:

  • group-hover:text-blue-500: Ändrar textfärgen på rubriken när användaren hovrar över gruppen.
  • group-hover:text-gray-800: Ändrar textfärgen på paragrafen vid samma tillstånd.

Exempel: Interaktiva kort

Här är ett exempel på ett interaktivt kort som använder flera pseudo-klasser:

<div class="max-w-sm mx-auto bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow">
  <img class="w-full h-48 object-cover" src="https://via.placeholder.com/150" alt="Bild">
  <div class="p-4">
    <h2 class="text-lg font-bold text-gray-800 hover:text-blue-500">Interaktiv rubrik</h2>
    <p class="text-gray-600">Detta är ett exempel på ett interaktivt kort.</p>
    <button class="mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 focus:ring-2 focus:ring-blue-300">
      Läs mer
    </button>
  </div>
</div>

Förklaring:

  • hover:shadow-lg: Lägger till en större skugga när användaren hovrar över kortet.
  • hover:text-blue-500: Ändrar rubrikens färg vid hover.
  • focus:ring-2 focus:ring-blue-300: Lägger till en ring runt knappen vid fokus.

Övning: Skapa en interaktiv navigationsmeny

Uppgift

Skapa en navigationsmeny som innehåller:

  1. Flera länkar (t.ex. Hem, Om oss, Kontakt).
  2. En hover-effekt som ändrar textfärgen.
  3. En aktiv länk som har en annan stil (t.ex. fet text och annan färg).
  4. En gruppbaserad effekt där en ikon ändras när användaren hovrar över länken.

Krav

  • Använd Tailwinds pseudo-klasser för att hantera tillstånd.
  • Gör menyn responsiv så att den visas som en vertikal lista på små skärmar och en horisontell lista på större skärmar.

Exempel på HTML-struktur

<nav class="bg-gray-800 text-white p-4">
  <ul class="flex flex-col sm:flex-row sm:space-x-4">
    <li>
      <a href="#" class="block py-2 px-4 hover:text-blue-400 active:font-bold active:text-blue-500">
        Hem
      </a>
    </li>
    <li>
      <a href="#" class="block py-2 px-4 hover:text-blue-400 active:font-bold active:text-blue-500">
        Om oss
      </a>
    </li>
    <li>
      <a href="#" class="block py-2 px-4 hover:text-blue-400 active:font-bold active:text-blue-500 group">
        Kontakt
        <span class="ml-2 text-gray-400 group-hover:text-blue-400">📧</span>
      </a>
    </li>
  </ul>
</nav>

Utmaning

  • Lägg till en animation som gör att hover-effekten övergångar mjukt.
  • Gör menyn sticky så att den alltid är synlig högst upp på sidan.

Sammanfattning

  • Pseudo-klasser: Använd hover:, focus:, active:, och disabled: för att hantera olika tillstånd.
  • Gruppbaserade effekter: Använd group och group-hover: för att skapa interaktivitet mellan element.
  • Praktisk tillämpning: Skapa interaktiva komponenter som förbättrar användarupplevelsen.

För mer information, besök Tailwind CSS Dokumentation.

Tailwind CSS – Konfigurering och Anpassning

Introduktion

Tailwind CSS är mycket flexibelt och kan enkelt anpassas för att passa dina projektbehov. Genom att använda tailwind.config.js kan du:

  • Lägga till egna färger, typsnitt och spacing.
  • Skräddarsy breakpoints och andra designparametrar.
  • Utöka eller skriva över standardinställningarna.

I denna lektion går vi igenom hur du konfigurerar och anpassar Tailwind CSS, samt hur du kan skapa ett eget tema.


Skapa och konfigurera tailwind.config.js

När du installerar Tailwind CSS genereras en konfigurationsfil med kommandot:

npx tailwindcss init

Denna fil (tailwind.config.js) används för att anpassa Tailwind. Här är ett exempel på en grundläggande konfigurationsfil:

/** @type {import('tailwindcss').Config} */
export default {
  content: [
    "./index.html",
    "./src/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

Viktiga delar av konfigurationsfilen:

  1. content: Här anger du vilka filer Tailwind ska genomsöka för att hitta klasser. Detta är viktigt för att Tailwind ska kunna ta bort oanvänd CSS i produktion.
  2. theme: Här kan du anpassa eller utöka standardtemat.
  3. plugins: Här kan du lägga till Tailwind-plugins för extra funktionalitet.

Anpassa färger

Du kan lägga till egna färger eller skriva över standardfärgerna i theme.extend.colors:

theme: {
  extend: {
    colors: {
      brand: {
        light: '#3AB0FF',
        DEFAULT: '#007BFF',
        dark: '#0056B3',
      },
    },
  },
},

Användning:

<div class="bg-brand text-white p-4">
  Detta är en komponent med anpassad färg.
</div>

Anpassa typsnitt

Lägg till egna typsnitt i theme.extend.fontFamily:

theme: {
  extend: {
    fontFamily: {
      custom: ['"Open Sans"', 'sans-serif'],
    },
  },
},

Användning:

<p class="font-custom text-lg">
  Detta är text med ett anpassat typsnitt.
</p>

Anpassa spacing

Du kan lägga till egna spacing-värden (t.ex. padding och margin):

theme: {
  extend: {
    spacing: {
      '72': '18rem',
      '84': '21rem',
      '96': '24rem',
    },
  },
},

Användning:

<div class="p-72 bg-gray-200">
  Detta element har 18rem padding.
</div>

Anpassa breakpoints

Om du vill ändra standard-breakpoints kan du göra det i theme.screens:

theme: {
  screens: {
    sm: '480px',
    md: '768px',
    lg: '1024px',
    xl: '1280px',
    '2xl': '1536px',
  },
},

Lägg till plugins

Tailwind har ett stort ekosystem av plugins. Du kan lägga till plugins som @tailwindcss/forms för att förbättra formulärstilar:

npm install @tailwindcss/forms

Lägg sedan till pluginet i tailwind.config.js:

plugins: [
  require('@tailwindcss/forms'),
],

Exempel: Skapa ett anpassat tema

Här är ett exempel på en komplett konfigurationsfil med ett anpassat tema:

/** @type {import('tailwindcss').Config} */
export default {
  content: [
    "./index.html",
    "./src/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {
      colors: {
        brand: {
          light: '#3AB0FF',
          DEFAULT: '#007BFF',
          dark: '#0056B3',
        },
      },
      fontFamily: {
        custom: ['"Open Sans"', 'sans-serif'],
      },
      spacing: {
        '72': '18rem',
        '84': '21rem',
        '96': '24rem',
      },
    },
  },
  plugins: [
    require('@tailwindcss/forms'),
  ],
}

Övning: Skapa ett anpassat kort med eget tema

Uppgift

Skapa ett kort som använder ett anpassat tema. Kortet ska innehålla:

  1. En rubrik med ett anpassat typsnitt.
  2. En bakgrundsfärg från det anpassade färgschemat.
  3. Padding och margin från de anpassade spacing-värdena.
  4. En knapp med en hover-effekt som använder det anpassade färgschemat.

Exempel på HTML-struktur

<div class="max-w-md mx-auto bg-brand-light text-white p-72 rounded-lg shadow-lg">
  <h2 class="text-2xl font-custom">Anpassat kort</h2>
  <p class="mt-4">
    Detta kort använder ett anpassat tema med egna färger, typsnitt och spacing.
  </p>
  <button class="mt-6 px-4 py-2 bg-brand hover:bg-brand-dark rounded">
    Klicka här
  </button>
</div>

Krav

  • Lägg till de anpassade färgerna, typsnitten och spacing-värdena i tailwind.config.js.
  • Använd Tailwinds utility-klasser för att bygga kortet.

Utmaning

  • Lägg till en responsiv design så att kortet ändrar storlek på olika skärmstorlekar.
  • Lägg till en animation som gör att hover-effekten övergångar mjukt.

Sammanfattning

  • Konfiguration: Använd tailwind.config.js för att anpassa Tailwind CSS.
  • Anpassningar: Lägg till egna färger, typsnitt, spacing och breakpoints.
  • Plugins: Utöka funktionaliteten med Tailwind-plugins.
  • Praktisk tillämpning: Skapa komponenter som använder ditt anpassade tema.

För mer information, besök Tailwind CSS Dokumentation.

Bygga en Dashboardsida med Tailwind CSS

Introduktion

En dashboardsida är en central plats för att visa viktig information och ge användaren tillgång till olika funktioner. Med Tailwind CSS kan du snabbt bygga en responsiv och modern dashboardsida. I denna lektion går vi igenom:

  1. Hur du planerar en dashboardsida.
  2. Vilka element som bör vara med.
  3. Best practices för att bygga en dashboardsida.
  4. Ett praktiskt exempel.

Hur ska man tänka när man bygger en dashboardsida?

1. Definiera syftet

  • Vad är syftet med dashboarden? Är det att visa statistik, hantera användare eller något annat?
  • Identifiera de viktigaste funktionerna och informationen som användaren behöver.

2. Prioritera innehåll

  • Placera det viktigaste innehållet högst upp (t.ex. KPI:er, grafer eller notifikationer).
  • Använd en tydlig hierarki för att guida användaren.

3. Planera layouten

  • Sidhuvud (Header): För logotyp, navigering och användarprofil.
  • Sidomeny (Sidebar): För navigering mellan olika sektioner.
  • Huvudinnehåll (Main Content): För grafer, tabeller och annan information.
  • Sidfot (Footer): För länkar eller copyright-information.

4. Responsiv design

  • Se till att dashboarden fungerar bra på både desktop och mobila enheter.
  • Använd Tailwinds breakpoints (sm:, md:, lg:, etc.) för att anpassa layouten.

Vilka element borde vara med?

1. Sidhuvud (Header)

  • Logotyp.
  • Sökfält.
  • Användarprofil eller avatar.
  • Notifikationsikon.

2. Sidomeny (Sidebar)

  • Navigeringslänkar till olika sektioner (t.ex. Hem, Statistik, Inställningar).
  • Ikoner för att göra navigeringen tydligare.

3. Huvudinnehåll (Main Content)

  • KPI-kort: För att visa nyckeltal (t.ex. antal användare, försäljning).
  • Grafer och diagram: För att visualisera data.
  • Tabeller: För att visa detaljerad information.
  • Snabbåtgärder: Knapp för att lägga till ny data eller hantera inställningar.
  • Länkar till hjälp eller support.
  • Copyright-information.

Best Practices

  1. Håll det enkelt

    • Undvik att överbelasta dashboarden med för mycket information.
    • Använd whitespace för att skapa en luftig layout.
  2. Använd konsekventa färger

    • Använd ett färgschema som passar varumärket.
    • Använd Tailwinds färgklasser för att skapa kontrast och tydlighet.
  3. Responsivitet först

    • Börja med en mobilvänlig layout och bygg ut för större skärmar.
  4. Använd Tailwinds utility-klasser

    • Använd klasser som grid, flex, och space-x-4 för att snabbt skapa layouten.
  5. Testa användarupplevelsen

    • Säkerställ att dashboarden är lätt att navigera och att viktig information är lättillgänglig.

Exempel: Bygga en dashboardsida

Här är ett exempel på en enkel dashboardsida:

<div class="min-h-screen bg-gray-100 flex">
  <!-- Sidebar -->
  <aside class="w-64 bg-gray-800 text-white flex flex-col">
    <div class="p-4 text-lg font-bold">Dashboard</div>
    <nav class="flex-1">
      <ul>
        <li class="p-4 hover:bg-gray-700"><a href="#">Hem</a></li>
        <li class="p-4 hover:bg-gray-700"><a href="#">Statistik</a></li>
        <li class="p-4 hover:bg-gray-700"><a href="#">Inställningar</a></li>
      </ul>
    </nav>
    <div class="p-4">© 2025 Företag</div>
  </aside>

  <!-- Main Content -->
  <main class="flex-1 p-6">
    <!-- Header -->
    <header class="flex justify-between items-center mb-6">
      <h1 class="text-2xl font-bold">Välkommen, Användare</h1>
      <div class="flex items-center space-x-4">
        <input type="text" placeholder="Sök..." class="p-2 border rounded">
        <img src="https://via.placeholder.com/40" alt="Avatar" class="w-10 h-10 rounded-full">
      </div>
    </header>

    <!-- KPI Cards -->
    <section class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
      <div class="p-4 bg-white rounded shadow">
        <h2 class="text-lg font-bold">Användare</h2>
        <p class="text-2xl font-semibold">1,234</p>
      </div>
      <div class="p-4 bg-white rounded shadow">
        <h2 class="text-lg font-bold">Försäljning</h2>
        <p class="text-2xl font-semibold">45,678 kr</p>
      </div>
      <div class="p-4 bg-white rounded shadow">
        <h2 class="text-lg font-bold">Supportärenden</h2>
        <p class="text-2xl font-semibold">12</p>
      </div>
    </section>

    <!-- Table -->
    <section class="bg-white rounded shadow p-4">
      <h2 class="text-lg font-bold mb-4">Senaste aktiviteter</h2>
      <table class="w-full text-left">
        <thead>
          <tr>
            <th class="border-b p-2">Datum</th>
            <th class="border-b p-2">Aktivitet</th>
            <th class="border-b p-2">Status</th>
          </tr>
        </thead>
        <tbody>
          <tr>
            <td class="p-2">2025-08-19</td>
            <td class="p-2">Ny användare registrerad</td>
            <td class="p-2 text-green-500">Slutförd</td>
          </tr>
          <tr>
            <td class="p-2">2025-08-18</td>
            <td class="p-2">Supportärende skapat</td>
            <td class="p-2 text-yellow-500">Pågående</td>
          </tr>
        </tbody>
      </table>
    </section>
  </main>
</div>

Övning: Bygg din egen dashboardsida

Uppgift

Bygg en egen dashboardsida som innehåller:

  1. En sidomeny med minst tre länkar.
  2. Ett sidhuvud med en sökfält och en användarprofil.
  3. Tre KPI-kort som visar nyckeltal.
  4. En tabell som visar data.

Krav

  • Använd Tailwinds utility-klasser för layout och styling.
  • Gör sidan responsiv så att den fungerar på både mobil och desktop.

Utmaning

  • Lägg till en graf eller diagram med hjälp av ett bibliotek som Chart.js.
  • Lägg till en hover-effekt på sidomenyns länkar.

Sammanfattning

  • Planering: Definiera syftet och prioritera innehållet.
  • Element: Inkludera sidhuvud, sidomeny, KPI-kort, tabeller och grafer.
  • Best practices: Håll det enkelt, använd konsekventa färger och bygg responsivt.

För mer information, besök Tailwind CSS Dokumentation.

Tailwind CSS – Best Practices & Komponentbibliotek

Introduktion

Tailwind CSS är ett kraftfullt verktyg för att bygga moderna och responsiva gränssnitt. För att maximera effektiviteten och skapa skalbara projekt är det viktigt att följa best practices och dra nytta av komponentbibliotek. I denna lektion går vi igenom:

  1. Best practices för att arbeta med Tailwind CSS.
  2. Hur du använder och anpassar komponentbibliotek.
  3. Ett praktiskt exempel.

Best Practices för Tailwind CSS

1. Använd återanvändbara komponenter

  • Skapa återanvändbara komponenter för vanliga UI-element som knappar, kort och formulär.
  • Använd Tailwinds @apply-direktiv i dina CSS-filer för att gruppera klasser:
/* filepath: src/styles/components.css */
.btn {
  @apply px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600;
}

Använd sedan klassen i HTML:

<button class="btn">Klicka här</button>

2. Håll HTML-strukturen ren

  • Undvik att lägga för många klasser direkt i HTML. Använd komponentklasser eller utility-klasser strategiskt.
  • Exempel:
<!-- Istället för detta -->
<button class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
  Klicka här
</button>

<!-- Använd en komponentklass -->
<button class="btn">Klicka här</button>

3. Använd Tailwinds konfigurationsfil

  • Anpassa Tailwind i tailwind.config.js för att skapa ett konsekvent design-system.
  • Lägg till egna färger, typsnitt och spacing:
theme: {
  extend: {
    colors: {
      brand: '#007BFF',
    },
    fontFamily: {
      custom: ['"Open Sans"', 'sans-serif'],
    },
  },
},

4. Responsiv design först

  • Börja med en mobilvänlig layout och bygg ut för större skärmar med Tailwinds breakpoints (sm:, md:, lg:, etc.).
  • Exempel:
<div class="p-4 sm:p-6 lg:p-8">
  Responsiv padding
</div>

5. Använd plugins

  • Dra nytta av Tailwinds ekosystem av plugins för att utöka funktionaliteten, t.ex.:
    • @tailwindcss/forms för formulärstilar.
    • @tailwindcss/typography för bättre textformatering.
    • @tailwindcss/aspect-ratio för att hantera bildförhållanden.

Komponentbibliotek för Tailwind CSS

Tailwind CSS har flera färdiga komponentbibliotek som kan hjälpa dig att snabbt bygga gränssnitt. Här är några populära alternativ:

1. Tailwind UI

  • Ett officiellt komponentbibliotek från skaparna av Tailwind CSS.
  • Innehåller högkvalitativa komponenter för allt från knappar till kompletta layouter.
  • Besök Tailwind UI

2. Flowbite

  • Ett open-source komponentbibliotek som fokuserar på interaktiva komponenter som modaler, dropdowns och navigationsmenyer.
  • Besök Flowbite

3. DaisyUI

  • Ett plugin för Tailwind CSS som lägger till färdiga komponenter och teman.
  • Enkel att använda och anpassa.
  • Besök DaisyUI

4. Headless UI

  • Ett bibliotek med tillståndsfria och tillgängliga komponenter som dropdowns, modaler och flikar.
  • Perfekt för att bygga egna komponenter med Tailwind.
  • Besök Headless UI

Exempel: Bygga en komponent med DaisyUI

Här är ett exempel på hur du kan använda DaisyUI för att skapa en knapp:

Installation

Installera DaisyUI:

npm install daisyui

Lägg till DaisyUI i tailwind.config.js:

plugins: [
  require('daisyui'),
],

Användning

Använd DaisyUIs färdiga klasser för att skapa en knapp:

<button class="btn btn-primary">Primär knapp</button>

Övning: Skapa en komponent med Tailwind CSS

Uppgift

Skapa en återanvändbar kortkomponent som innehåller:

  1. En bild.
  2. En rubrik.
  3. En kort beskrivning.
  4. En knapp.

Krav

  • Använd Tailwinds @apply-direktiv för att skapa en komponentklass i CSS.
  • Gör kortet responsivt så att det ser bra ut på både mobil och desktop.

Exempel på HTML-struktur

<div class="card">
  <img src="https://via.placeholder.com/150" alt="Bild" class="card-img">
  <div class="card-body">
    <h2 class="card-title">Korttitel</h2>
    <p class="card-text">Detta är en kort beskrivning.</p>
    <button class="card-btn">Läs mer</button>
  </div>
</div>

Exempel på CSS

/* filepath: src/styles/components.css */
.card {
  @apply bg-white rounded-lg shadow-md overflow-hidden;
}
.card-img {
  @apply w-full h-48 object-cover;
}
.card-body {
  @apply p-4;
}
.card-title {
  @apply text-lg font-bold text-gray-800;
}
.card-text {
  @apply text-gray-600;
}
.card-btn {
  @apply mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600;
}

Sammanfattning

  • Best Practices: Använd återanvändbara komponenter, håll HTML-strukturen ren och dra nytta av Tailwinds konfigurationsfil.
  • Komponentbibliotek: Utforska bibliotek som Tailwind UI, Flowbite och DaisyUI för att snabba upp utvecklingen.
  • Praktisk tillämpning: Skapa egna komponenter med Tailwinds utility-klasser och @apply.

För mer information, besök Tailwind CSS Dokumentation.

Arbeta med Tailwind CSS och React med Vite

Introduktion

Att använda Tailwind CSS tillsammans med React och Vite är ett effektivt sätt att bygga moderna och responsiva webbapplikationer. Vite är ett snabbt byggverktyg som gör utvecklingsprocessen smidig, medan Tailwind CSS erbjuder utility-first-klasser för att snabbt styla komponenter.

I denna lektion går vi igenom:

  1. Hur du installerar och konfigurerar Tailwind CSS i ett React-projekt med Vite.
  2. Hur du använder Tailwind CSS i React-komponenter.
  3. Best practices för att arbeta med Tailwind och React.

Steg 1: Skapa ett React-projekt med Vite

Skapa ett nytt projekt

Skapa ett nytt React-projekt med Vite:

npm create vite@latest my-app --template react
cd my-app
npm install

Installera Tailwind CSS

Installera Tailwind CSS och dess beroenden:

npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

Konfigurera Tailwind

Uppdatera tailwind.config.js för att inkludera alla filer där du använder Tailwind-klasser:

// filepath: tailwind.config.js
/** @type {import('tailwindcss').Config} */
export default {
  content: [
    "./index.html",
    "./src/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

Lägg till Tailwind i din CSS

Skapa eller uppdatera filen src/index.css och inkludera Tailwinds direktiv:

@tailwind base;
@tailwind components;
@tailwind utilities;

Starta projektet

Starta utvecklingsservern för att verifiera att Tailwind fungerar:

npm run dev

Steg 2: Använda Tailwind CSS i React-komponenter

Exempel: Skapa en knappkomponent

Här är ett exempel på en enkel knappkomponent som använder Tailwind CSS:

// filepath: src/components/Button.jsx
export default function Button({ text, onClick }) {
  return (
    <button
      className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
      onClick={onClick}
    >
      {text}
    </button>
  );
}

Exempel: Skapa en kortkomponent

Här är ett exempel på en kortkomponent som använder Tailwind CSS:

// filepath: src/components/Card.jsx
export default function Card({ title, description }) {
  return (
    <div className="max-w-sm bg-white rounded-lg shadow-md overflow-hidden">
      <div className="p-4">
        <h2 className="text-lg font-bold text-gray-800">{title}</h2>
        <p className="text-gray-600">{description}</p>
      </div>
    </div>
  );
}

Använd komponenterna i din app

Importera och använd komponenterna i din App.jsx:

// filepath: src/App.jsx
import Button from "./components/Button";
import Card from "./components/Card";

function App() {
  return (
    <div className="p-6 bg-gray-100 min-h-screen">
      <h1 className="text-2xl font-bold text-gray-800 mb-4">Välkommen till min app</h1>
      <Card
        title="Korttitel"
        description="Detta är en beskrivning av kortet."
      />
      <Button text="Klicka här" onClick={() => alert("Knappen klickades!")} />
    </div>
  );
}

export default App;

Best Practices för Tailwind CSS och React

1. Håll komponenterna små och återanvändbara

  • Dela upp din kod i små, återanvändbara komponenter.
  • Exempel: Skapa separata komponenter för knappar, kort, formulärfält, etc.

2. Använd Tailwinds konfigurationsfil

  • Anpassa Tailwind i tailwind.config.js för att skapa ett konsekvent design-system.
  • Lägg till egna färger, typsnitt och spacing.

3. Använd conditional classnames

  • Använd bibliotek som clsx eller classnames för att hantera dynamiska klasser i React:
npm install clsx

Exempel:

import clsx from "clsx";

export default function Button({ isPrimary }) {
  return (
    <button
      className={clsx(
        "px-4 py-2 rounded",
        isPrimary ? "bg-blue-500 text-white" : "bg-gray-200 text-black"
      )}
    >
      Klicka här
    </button>
  );
}

4. Använd Tailwinds plugins

  • Dra nytta av Tailwinds plugins för att utöka funktionaliteten, t.ex.:
    • @tailwindcss/forms för formulärstilar.
    • @tailwindcss/typography för bättre textformatering.

5. Håll HTML-strukturen ren

  • Undvik att lägga för många klasser direkt i JSX. Använd komponentklasser eller utility-klasser strategiskt.

Övning: Bygg en enkel profilkomponent

Uppgift

Skapa en profilkomponent som innehåller:

  1. En profilbild.
  2. Ett namn och en titel.
  3. En knapp för att följa användaren.

Krav

  • Använd Tailwinds utility-klasser för styling.
  • Gör komponenten responsiv så att den ser bra ut på både mobil och desktop.

Exempel på HTML-struktur

// filepath: src/components/ProfileCard.jsx
export default function ProfileCard({ name, title, image }) {
  return (
    <div className="max-w-sm bg-white rounded-lg shadow-md overflow-hidden">
      <img className="w-full h-48 object-cover" src={image} alt={name} />
      <div className="p-4">
        <h2 className="text-lg font-bold text-gray-800">{name}</h2>
        <p className="text-gray-600">{title}</p>
        <button className="mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
          Följ
        </button>
      </div>
    </div>
  );
}

Använd komponenten

Importera och använd komponenten i din app:

// filepath: src/App.jsx
import ProfileCard from "./components/ProfileCard";

function App() {
  return (
    <div className="p-6 bg-gray-100 min-h-screen">
      <ProfileCard
        name="John Doe"
        title="Webbutvecklare"
        image="https://via.placeholder.com/150"
      />
    </div>
  );
}

export default App;

Sammanfattning

  • Installation: Installera och konfigurera Tailwind CSS i ditt React-projekt med Vite.
  • Komponenter: Använd Tailwind CSS för att snabbt styla React-komponenter.
  • Best Practices: Håll komponenterna små och återanvändbara, använd conditional classnames och dra nytta av Tailwinds plugins.

För mer information, besök Tailwind CSS Dokumentation och React Dokumentation.