Slik ser det ut når jeg utvikler uten ekstern skjerm.
Slik ser det ut når jeg utvikler uten ekstern skjerm. Vis mer

Slik bygde jeg frontend til kodekonkurransen

Hvorfor jeg valgte React når jQuery kunne vært nok, og hvordan mobilen gjorde opprør.

Sent tirsdag forrige uke publiserte kode24-redaksjonen vår aller første kodekonkurranse.

Målet med konkurransen? Å ha noe gøy å fylle kode24.no med frem til lansering. Vi ville få fram vår humor, og engasjere dere lesere.

Og hva hadde vel kode24 vært hvis ikke vi delte koden vår med dere? Nøyaktig derfor ønsker jeg å dele litt om hvordan jeg programmerer, og verktøyene jeg foretrekker.

I denne artikkelen tar jeg for meg frontenden av applikasjonen. Senere vil jeg dekke backend, stiling av applikasjonen og hvordan den er satt i produksjon.

Og hvis du ikke rakk å prøve konkurranse, er den fortsatt oppe på Heroku. Vil du se på koden? Last den ned fra Github her.

Idéen bak

Det er ingen hemmelighet at både jeg og Ole Petter har en forkjærlighet for datamaskiner fra 80- og 90-tallet. Dere som har sett pressebildene våre vet at rare gamle terminaler er noe vi vet å sette pris på.

Dessuten er vi begge vokst opp byen som er mest kjent for porselenstoaletter; Porsgrunn. Vi hadde lyst til å lage en konkurranse som fleipet med hjembyen vår, gamle terminaler av typen DOS, og lanseringen av kode24.

Dessuten hadde vi nylig fått inn et knippe stilige klistremerker vi ville dele bort.

Falsk filstruktur

Vi landet altså på et konsept hvor brukeren skulle navigere i en fiktiv filstruktur, i et fiktivt DOS-språk, på en fiktiv terminalserver, tilhørende Tom Jeremiassen. Tom skulle være en fiktiv serveradmin fra Porsgrunn, med mye privat innhold på brukerkontoen sin.

For å drodle en fiktiv filstruktur startet jeg med en JSON-fil. Det er noe jeg ofte gjør, før jeg har en backend på plass. Da kan jeg simulere API-kall fra frontend ved å peke på JSON-fila, og senere bytte den ut med ekte API-adresser.

{
  "mappenavn": {
    {
      "folders": {
        "mappenavn": {
          "folders": {},
          "documents": {}
        }
      },
      "documents": {
        "filnavn": [
          { "type": "innholdstype", content: "tekstinnhold"  }
        ]
      }
    }    
  }
}

Planen var en enkel struktur av objekter hvor «home» er øverste nivå og hvert nivå inneholder nøklene «folders» og «documents». Under «folders»-nøkkelen nøstes nye nøkler med nye «folders» og «documents». Innenfor «documents» ligger fiktive filer som egentlig består av et array med objekter, ett for hver linje som skal skrives ut.

"caesar-fans": {
            "folders": {},
            "documents": {
              "index.html": [
                {"type": "regular", "content": "<!DOCTYPE HTML><HTML><HEAD>"},
                {"type": "regular", "content": "<TITLE>Fansite for tidenes tv-serie</TITLE>"},
                {"type": "regular", "content": "</HEAD><BODY>"},
                {"type": "regular", "content": "<!-- Personlig prosjekt Tom -->"},
                {"type": "regular", "content": "<TABLE><TR>"},
                {"type": "regular", "content": "<TH>NAVN</TH>"},
                {"type": "regular", "content": "<TH>SEXYHET</TH>"},
                {"type": "regular", "content": "<TH>ULEMPER</TH>"},
                {"type": "regular", "content": "</TR><TR>"},
                {"type": "regular", "content": "<TD>Astrid Anker-Hansen</TD>"},
                {"type": "regular", "content": "<TD>8/10</TD>"},
                {"type": "regular", "content": "<TD>RULLESTOL OG HÅR SOM EN ALIEN</TD>"},
                {"type": "regular", "content": "</TR><TR>"},
                {"type": "regular", "content": "<TD>julie Anker-Hansen</TD>"},
                {"type": "regular", "content": "<TD>11/10</TD>"},
                {"type": "regular", "content": "<TD>feilfri</TD>"},
                {"type": "regular", "content": "</TR></BODY></HTML>"},
                {"type": "regular", "content": "-------END OF FILE---------"}
              ]
            } 
          },

Abstrakt frontend

Min tanke var at frontend skulle vite så lite som mulig om innholdet i det fiktive filsystemet. Med andre ord skulle frontend utelukkende ta imot tegn fra tastaturet, tegne til skjerm, og be backend om data, uten å vite mye om hva som kom tilbake.

Jeg antok nemlig at enkelte av dere ville jukse ved å undersøke Javascript-koden. 😎

Frontenden til kodekonkurransen er så enkel at det kunne vært skrevet i jQuery, uten problemer. Jeg valgte likevel React, siden appen hovedsakelige skal tegne en haug med HTML på skjermen og holde oversikt over tilstanden til brukeren i det fiktive filsystemet. Perfect for React!

Aldri brukt React før? Da anbefaler jeg en tur innom React sin egen nybegynner-tutorial.

I denne applikasjonen brukte jeg en verktøysamling som heter create-react-app. Den gir deg alt du trenger for å skrive og bygge en moderne React-app, og anbefales for alle som vil spinne opp en kjapp applikasjon.

I tillegg bruker jeg rammeverket Styled Components for CSS. Det skal jeg komme nærmere inn på senere, men jeg har blitt småforelsket i det!

Komponenter

React-appen min består av to enkle komponenter, «mesterkomponenten» og «input-komponenten». «Mesterkomponent» er begrepet jeg bruker for filer som «kommer til å ese ut og måtte deles opp i flere filer på sikt» 🔪

«Mesterkomponenten» vedlikeholder et array med linjene som vises på skjermen i kodekonkurransen. Avhengig av hva som skjer i komponenten legger den til linjer og sørger for at de blir tegnet opp i applikasjonen. Den vedlikeholder også et array som sier hvor i filsystemet brukeren befinner seg.

//mesterkomponenten holder linjene og stien
 class Master extends Component {
  state = {
    lines: [],
    path: ["home"]
  } ...
«Input-komponenten» lytter til beskjeder fra brukeren. Foto: Jørgen Jacobsen
«Input-komponenten» lytter til beskjeder fra brukeren. Foto: Jørgen Jacobsen Vis mer

«Input-komponenten» har i oppgave å lytte til tasting fra brukeren, og sende kommandoer videre til «mesterkomponenten». Når brukeren trykker på enter-tasten blir kommandoen sendt. «Input-komponenten» tegner også feltet hvor brukeren ser hva hun skriver.

//Input-komponenten holder tastetrykk
// og register over tidligere kommandoer
class Input extends Component {
  state = {
    characters: "",
    previousInputs: [],
    hasFocus: false
  };
...

I tilegg har jeg opprettet en egen fil med funksjoner for å snakke med API-et.

// En av flere funksjoner i API-hjelperfila
//I backend bruker jeg errorkode 404 som tegn på at brukeren
//prøver å gjøre noe som backend ikke godtar.
//Merk at 404 ikke kaster error som default
const getContentsOfFile = (path, file, success, fail) => {
    fetch(('/api/filesystem/' + path + "?command=print&file=" + file).toLowerCase(), {
        method: 'get',
        headers:{
            'Content-Type': 'application/json'
        }
    })
    .then(response => {
        if (response.ok) {
          return response.json();
        } else {
          throw new Error('404');
        }
      })
    .then(response => {
        success(response);
    })
    .catch(error => fail(error));
}

Personlig foretrekker jeg en programmeringsstil hvor jeg har færrest mulig filer i starten av et prosjekt. Når filene eser ut og blir gigantiske deler jeg dem gjerne opp i mindre filer, gjerne en til hver komponent. Jeg er fan av filstrukturen React-utvikler Dan Abramov anbefaler. 😍

Tasting til besvær

Som jeg skrev tidligere lagde jeg en egen komponent for å lytte til tasting fra brukeren. Denne komponenten fikk samtidig ansvaret for å tegne ut ordene brukeren skrev inn. Når brukeren trykker «enter» skal teksten sendes over til «mesterkomponenten» for tolking.

//Lytt til tastetrykk og send dem til handleKeyDown
componentWillMount () {
    document.addEventListener("keydown", this.handleKeyDown.bind(this));
  }

Først skrev jeg en enkel event-lytter, som lyttet på tastetrykk fra hele nettsiden. Deretter lagret jeg alle tastetrykk i et array, og lot React tegne en «span» med tastetrykkene slått sammen til en streng. Fordelen med en span er at den er inline og dermed utvider seg ettersom bokstaver fylles inn. Da kunne jeg tegne en blinkende boks rett etter, slik som på en gammel datamaskin.

- Kan du ikke bare vise et input-felt, tenker du?

Neida, input-felt har en fast bredde. Derfor kan vi ikke vite hvor enden av teksten er.

Det skulle vise seg at denne løsningen ikke var særlig holdbar.

Mobilen gjorde opprør

I starten funket det superdupert på desktop, men på mobil? «Not so much».

For å få opp tastaturet på mobil må man nemlig ha fokus på et input-felt. Ikke nok med det: Fra mobilenheter er det kun to måter å gi fokus til et input-felt. Enten må brukeren fysisk trykke i feltet, eller så må man hente ut input-elementet i Javascript og sette fokus. Det er bare at det er en hake. Mobile nettleseren tillater ikke Javascript å sette fokus med mindre brukeren har trigget et klikk-event.

Den fiktive login-skjermen brukes til å fange et klikk-event fra brukeren. Foto: Jørgen Jacobsen
Den fiktive login-skjermen brukes til å fange et klikk-event fra brukeren. Foto: Jørgen Jacobsen Vis mer

Du la kanskje merke til at du måtte klikke på en sort knapp på en startskjerm når kodekonkurransen startet? Det klikket bruker jeg til å sette fokus på input-boksen, slik at tastaturet dukker opp på mobil. Fiffig, og rimelig teit, ikke sant? 🤓

Til slutt bestemte jeg meg for å droppe hele lyttingen på tastaturet og heller lytte på endringer i input-feltet direkte. Det viste seg nemlig at Chrome på Android har ganske grumsete støtte for taste-eventer.

Jeg hadde egentlig tenkt å flytte input-feltet utenfor skjermen, slik at brukeren ikke så det. Det fungerte ypperlig på Android, men Safari på iOS bestemte seg jaggu for å flytte feltet inn på skjermen igjen.

// Sette fokus på elementet
// Trigges av et klikk-event  
focusInput () {
    this.inputRef.current.focus();
    this.setState({
      hasFocus: true
    });
  }

Dermed stilet jeg om input-feltet, droppet den blinkende boksen etter siste tastetrykk og gikk for å vise det originale feltet til brukeren. Det førte til at blinkeren etter teksten ble blå på iOS. Det finnes faktisk en egen CSS-velger for dette, «caret-color: #fff;», men den støtter ikke Safari.

Den eneste lyttingen jeg beholdt for tastaturet var enter-tasten, som av uforståelige grunner fungerer på alle platformer.

iOS insisterer på å ha blåfarget blinkende boks. Foto: Jørgen Jacobsen
iOS insisterer på å ha blåfarget blinkende boks. Foto: Jørgen Jacobsen Vis mer

Koble alt sammen

Til slutt hadde jeg altså en «input-komponent» som lot brukeren taste inn kommandoer. Jeg lyttet på enter-tasten, og sendte kommandoene brukeren skrev inn til «mesterkomponenten» for tolking.

Jeg lot «mesterkomponenten» tolke kommandoene fra «input-komponentet». Derfra kunne den bestemme om den ville godta en kommando og kalle på API-et, eller skrive ut en feilmelding. Alt dette ble altså gjort ved å manipulere et array som React lyttet på.

Det var alt for denne gang. Neste gang går jeg i detalj rundt CSS-en jeg skrev for konkurransen. Følg med!

Og vil du se koden til konkurransen? Den finner du på Github.