Skip to content

Instantly share code, notes, and snippets.

@arildm
Last active February 23, 2022 13:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save arildm/16834e6933151667dac395c9d2ed6773 to your computer and use it in GitHub Desktop.
Save arildm/16834e6933151667dac395c9d2ed6773 to your computer and use it in GitHub Desktop.
Anteckningar inför workshop i Vue.js 3

Vue.js 3 workshop

Anteckningar

Prerequisites

  • Node.js ≥16, npm 8

Init

  1. npm init vue@latest
    • Välj No på allt
  2. cd <projektnamn>
  3. npm install
  4. npm run dev

Komponenter

Filändelsen *.vue:

  • <script> (JavaScript)
  • <template> (HTML-baserad template)
  • <style> (CSS)

Komponenternas abstrahering

CDN

const HelloWorld = {
  data() {
    return {
      msg: 'Hello!',
    };
  },
  template: `<h1>{{ msg }}</h1>`,
};
  • template som en vanlig sträng

Single-file components (SFC)

<script>
export default const {
  data() {
    return {
      msg: 'Hello!',
    };
  },
}
</script>

<template>
  <h1>{{ msg }}</h1>
</template>
  • standard i Vue 2

SFC med Composition API

<script>
import { ref } from 'vue';

export default const {
  setup() {
    const msg = ref('Hello!');

    return { msg };
  },
};
</script>

<template>
  <h1>{{ msg }}</h1>
</template>
  • nya Composition API utan socker

SFC med <script setup>

<script>
import { ref } from 'vue';

const msg = ref('Hello!');
</script>

<template>
  <h1>{{ msg }}</h1>
</template>
  • socker för Composition API

Ta bort defaultkod

  1. Ta bort alla komponenter utom App.vue, och ta bort logo.svg
  2. Ersätt hela template-innehållet i App.vue med Tomt!
  3. Ta bort all CSS i App.vue utom @import ... och #app {...}

Egen komponent

Skapa src/components/Resource.vue:

<template>
	<div class="resource">
		<h2>Aftonbladet 1900-talet</h2>
		<p>Del av samlingen Kubhist2</p>
	</div>
</template>

<style scoped>
.resource {
  background: #ded;
  border-radius: .5em;
  padding: 1em;
  margin-bottom: 1em;
}
</style>

App.vue:

import Resource from './components/Resource.vue'
<Resource />

Props

Resource.vue:

defineProps({
	name: String,
	description: String,
})
<h2>{{ name }}</h2>
<p>{{ description }}</p>

App.vue:

<Resource
	name="Aftonbladet 1900-talet"
	description="Del av samlingen Kubhist2"
	/>

v-for, :attr

Undersök datastrukturen i API:et: https://ws.spraakbanken.gu.se/ws/metadata/corpora

const resources = [
	{
		"name_sv": "Aftonbladet 1900-talet",
		"description_sv": "Del av samlingen Kubhist2",
	},
	{
		"name_sv": "Alfwar och Skämt 1840-talet",
		"description_sv": "Del av samlingen Kubhist2",
	},
]
<Resource
	v-for="resource in resources"
	:key="resource.name_sv"
	:name="resource.name_sv"
	:description="resource.description_sv" />
  • Kolon : för att ange JS-uttryck i props
  • Vue 2: key inte längre nödvändigt, men behövs i regel om listan ska uppdateras dynamiskt

Ladda data över HTTP

App.vue:

async function loadResources() {
	const response = await fetch('https://ws.spraakbanken.gu.se/ws/metadata/corpora')
	const data = await response.json()
	resources = data.resources
}
loadResources()
  • async-await
  • körs när komponent-instansen skapas
    • Vue 2: created()
import { ref } from 'vue'
  
const resources = ref([])

  // ...
  resources.value = data.resources
  • Wrappa i ref: template kan lyssna på förändringar
  • resources.value för att läsa/skriva en ref-variabel
  • Vue 2: data()

Events, v-if

Vill att man ska kunna fälla ut/in varje ruta vid klick.

  • State: expanded

Resource.vue:

const expanded = false

<p v-if="expanded">
  • v-if: <p>-elementet uteblir annars
const expanded = ref(false)

function toggle() {
	expanded.value = !expanded.value
}
  • argumentet till ref() är det initiala värdet
  • Vue 2: methods
<div @click="toggle">
	<h2>{{ name }}</h2>
	<p v-if="expanded">{{ description }}</p>
</div>
  • @ lyssna

v-else

Vill ha en indikator för utfällt läge

  <div @click="toggle" class="resource">
    <div class="toggle-marker">
      <template v-if="expanded">-</template>
      <template v-else>+</template>
    </div>
.toggle-marker {
  float:right;
	font-size: larger;
}

:style

Resource.vue:

<div ... :style="{backgroundColor: expanded ? '#dde' : '#ded'}">

<h2 :style="{fontWeight: expanded ? 'normal' : 'bold'}">
  • ett objekt med camelCase istf kebab-case

:class

Vill separera CSS och undvika style-attribut

Resource.vue:

.collapsed {
	background-color: #dde;
}
  
.collapsed h2 {
	font-weight: bold;
}
<div ... :class="expanded ? '' : 'collapsed'">
  • Kan använda :class parallellt med class
<div ... :class="{collapsed: !expanded}">
  • objekt med [klassnamn]: [bool]

computed

Vill filtrera på "19" t ex

const filterResource = a => a.name_sv.includes("19")
const resourcesFiltered = computed(() => resources.value.filter(filterResource))
v-for="resource in resourcesFiltered"
  • Vue 2: computed: { hasDescription: () => ... }

v-model

Vill modifiera filtret med formulär

App.vue:

  <div class="filter-form">
    <h2>Sök</h2>
    <input v-model="filter" />
    {{ filter }}
  </div>
const filter = ref('')
  • funkar på formulärelementen: input, select, textarea
const filterResource = resource => resource.name_sv.includes(filter.value)
.filter-form {
  background-color: #dee;
  padding: 1em;
  margin-bottom: 2em;
}

Composition

  • Extrahera toggle/expand-bitar till egen "composable"
    • gärna "stateful" logik
  • Mönstret är att exportera en funktion useX() som instansierar bitarna
    • innehåller det <script setup> innehåller
    • returnerar flera saker som objekt

src/composables/toggle.js:

import { ref } from 'vue'

export default function useToggle()  // 1
	const expanded = ref(false)        // 2

	function toggle() {                // 3
		expanded.value = !expanded.value
	}

	return {
		expanded,                        // 2
		toggle,                          // 3
	}
}

Resource.vue:

import useToggle from '../composables/toggle'

const { expanded, toggle } = useToggle()

App.vue:

import useToggle from './composables/toggle'

const { expanded, toggle } = useToggle()
<div class="filter-form" @click="toggle">
	<div class="toggle-marker">
	  <template v-if="expanded">-</template>
	  <template v-else>+</template>
	</div>
  
  <input v-if="expanded" v-model="filter" />

ToggleMarker

src/composables/TogglerMarker.vue:

<script setup>
defineProps({
	expanded: Boolean,
})
</script>
  
<template>
	<div class="toggle-marker">
		<template v-if="expanded">-</template>
		<template v-else>+</template>
	</div>
</template>
  
<style>
.toggle-marker {
	float:right;
	font-weight: bold;
}
</style>

toggle.js:

import ToggleMarker from './ToggleMarker.vue

return {
  // ...
  ToggleMarker,
}

Resource.vue:

<ToggleMarker v-if="hasDescription" :expanded="expanded" />

App.vue:

<ToggleMarker :expanded="expanded" />

@click.stop

  • Nu kan man inte använda textfältet

App.vue:

<input v-if="expanded" v-model="filter" @click.stop />
  • Lyssna på click men utan någon särskild handler
  • Däremot med .stop som socker för event.stopPropagation()
    • click-eventet når då inte överordnade element

useResources

src/composables/resources.js:

import { computed, ref } from 'vue'

export default function useResources(filterRef) {
  const resources = ref([])

  const filterResource = resource => resource.name_sv.includes(filterRef.value)
  const resourcesFiltered = computed(() => resources.value.filter(filterResource))

  const compareResources = (a, b) => a.name_sv.localeCompare(b.name_sv, 'sv')
  const resourcesSorted = computed(() => resourcesFiltered.value.sort(compareResources))

  async function loadResources() {
    const response = await fetch('https://ws.spraakbanken.gu.se/ws/metadata/corpora')
    const data = await response.json()
    resources.value = data.resources
  }
  loadResources()

  return {
    resources: resourcesSorted
  }
}

App.vue:

import useResources from './composables/resources'

const { resources } = useResources(filter)
v-for="resource in resources"

watchEffect

App.vue:

<select v-if="expanded" v-model="type" @click.stop>
	<option>corpora</option>
	<option>lexicons</option>
	<option>models</option>
</select>
  • Sätter textinnehållet som värde, om man inte sätter value=""
const type = ref('corpora')

const { resources } = useResources(type, filter)

resources.js:

export default function useResources(typeRef, filterRef) {


  const response = await fetch('https://ws.spraakbanken.gu.se/ws/metadata/' + typeRef.value)


// -  loadResources()
watchEffect(() => loadResources())
  • watchEffect märker av vilka reaktiva variabler som används
    • när variablerna ändras körs funktionen igen
  • Vue 2: watch: { type() {...} }
    • Samma mekanism finns i Vue 3: watch(typeRef, () => {...})

slots

Säg att vi vill välja någon resurs för att se all info till höger.

Columns.vue:

<template>
	<div class="columns">
		<div class="left">
			<slot name="left" />
		</div>
		<div class="right">
			<slot name="right" />
		</div>
	</div>
</template>
  
<style>
.columns {
	display: flex;
}
  
.left {
	width: 50%;
	padding-right: 1em;
}
  
.right {
	width: 50%;
	padding-left: 1em;
}
</style>
  • <slot name=""> Här hamnar innehåll

App.vue:

import Columns from './components/Columns.vue';
<Columns>
	<template #left>
		<Resource ... />
  </template>

  <template #right>
    ...
  </template>
</Columns
  • #left: lägg innehållet i <slot name="left">
  • Vue 2: <template v-slot:left>

emit

Något händer i en komponent långt ner i trädet, och det ska propageras uppåt.

Resource.vue:

<h2>{{ name }}</h2>
<div v-if="expanded" @click.stop>
  <p>{{ description }}</p>
  <button @click="select">Visa</button>
</div>
const emit = defineEmits(['select'])

function select() {
  emit('select')
}

App.vue:

<Resource ... @select="showResource(resource.id)" />
const activeResource = ref(null)

function showResource(id) {
  activeResource.value = resources.value.find(resource => resource.id === id)
}
  <template #right>
    <pre>{{ activeResource }}</pre>
  </template>
  • Skriva ut objekt i template: automatiskt JSON.stringify
  • Vue 2: this.$emit(), behöver inte deklarera events

ResourceDetails

För att slippa detaljer i App.vue.

ResourceDetails.vue:

<script setup>
defineProps({
  resource: Object,
})
</script>

<template>
  <h1>{{ resource.name_sv }}</h1>
  <p>{{ resource.description_sv }}</p>

  <p>
    Språk:
    {{ resource.lang.map(lang => lang.name_sv).join(', ')}}
  </p>
</template>

App.vue:

import ResourceDetails from './components/ResourceDetails.vue';
<template #right>
	<ResourceDetails
		v-if="activeResource"
		:resource="activeResource" />
</template>

v-bind

App.vue:

<ResourceDetails ... v-bind="activeResource" />
  • Varje element i objektet activeResource blir ett attribut
  • De attribut som är props blir props

ResourceDetails.vue:

const props = defineProps({
	id: String,
	name_sv: String,
	description_sv: String,
	lang: Array,
	downloads: Array,
})
<h1>{{ name_sv }}</h1>
<p>{{ description_sv }}</p>
  
<p>
	Språk:
	{{ lang.map(lang => lang.name_sv).join(', ')}}
</p>
  • Mer i defineProps, mindre i template

v-html

Längre beskrivingstexter finns för vissa resurser om man lägger till ?resource=<id> i urlen.

ResourceDetails.vue:

const props = defineProps({
 id: String,
  • id finns där i objektet, bara deklarera prop:en för det
const long_description = ref('')

watchEffect(async () => {
  long_description.value = ''
  const response = await fetch('https://ws.spraakbanken.gu.se/ws/metadata/?resource=' + props.id)
  const data = await response.json()
  long_description.value = data.long_description_sv
})
  • Tom sträng '' om ingen lång text finns.
<div>{{ long_description }}</div>
  • HTML blir escaped!
<div v-html="long_description" />
  • OBS! Säkerhetsrisk. Man måste lita på källan.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment