Categories
Uncategorised

Vue

Templates

Vue templates end in .vue and can have three sections; template, script and style.

A lang attribute can be applied to the style tag to use other preprocessor languages;

<style lang="scss">

Styles included in a template are split out and combined into a separate CSS file. Using SCSS allows us to use import and SCSS language features within a template file;

@import "../../scss/components/light-component";
<style lang="scss">

Child Components

TODO!

Data

Initialise a Vue component with a data object. Return this from the data function when on initialisation, or include it in the script part of the .vue template file.

Data can be modified ‘live’, and will be re-rendered when the data changes. It’s a bit like KnockoutJS, but doesn’t have observer object nonsense and function calls to return the primitive value of data. Instead it uses Javascript’s getters and setters which are actually method calls on the object. Each data key defined will have a corresponding get and set method defined on the component.

Viewing a vue component in the console shows the getters and setters at the end of the object definition which all link to the same proxyGetter`` andproxyGetter“` methods. In Vue 3, a native Javascript proxy object is used instead of Vue’s own implementation.

Data values can be edited live in the browser. Installing Vue Browser Dev tools will provide an easier way to modify and debug functionality without assigning the app or component to a global window variable.

Data keys are accessible on the component as first party variables.

Props

Props are a way for a component to receive data in a read-only fashion. They don’t show in the Vue Dev tools and aren’t changeable. Think of props in a component like “arguments in a function” in PHP.

Props are accessible in templates in the same way as data keys, using curly bracket notation.

Props can be defined as an array or an object in our module using the following syntax. This allows us to specify types for props.

Array – no validation.

props: ['title'],

Objects #1 – define type

props: {
  title: String
}

Object 2 – with required

props: {
  title: {
    type: String,
    required: true
  }
}

Default Values

Use the default key when defining the prop;

props: {
  title: {
    type: String,
    required: true,
    default: "I'm the default value"
  }
}

It’s advisable to use the more verbose syntax and define types for props. We can also set a ‘required’ key here.

To pass a prop to a component, define it as an attribute on the component’s tag;

<legend-component title="Ooh matron!" />

Data Location ### 

Data should live in one place, on the deepest component which requires it, and then passed to child components via props.

When data is passed to a child component via a prop, the prop Should not be changed by the child. The value will update, but the parent will not be notified.

Communication UP with $emit (Events)

Broadcast events for parent components to listen to using

  methods: {
    toggleCollapsed() {
      this.$emit('toggle-collapsed');
    },
  },

Simple events can also be included inline in templates, rather than calling a method;

<button @click="$emit('toggle-collapsed')">Click</button>

this is automatically assumed when referencing properties in a template

We can then listen to this event when defining the component in a parent template using

<sidebar :collapsed="sidebarCollapsed" 
   @toggle-collapsed="toggleSidebarCollapsed" 
/>

@ is a shortcut for v-on:

  methods: {
    toggleSidebarCollapsed() {
      this.sidebarCollapsed = !this.sidebarCollapsed;
    },
  },

This passes :collapsed as a prop into the sidebar component. When the sidebar $emits the toggle-collapsed event, the toggleSidebarCollapsed method is called, this.sidebarCollapsed is updated, and propogated back to the sidebar component via the prop link.

Events being broadcast can be observed in the Vue Dev Tools’ Events tab in realtime.

v-bind: Dynamic Attributes

To define a dynamic value when passing in a prop, use v-bind. This uses the key from the data array as the value of the attribute, which must be prepended with v-bind:. v-bind has the shortcut of colon.

<legend-component v-bind:title="legend" />

This will take the “legend” value of the parent component’s data key and re-render the child component when it changes. The value in the attribute is actually a javascript expression, so things can be done such as;

<legend-component v-bind:title="legend + ' extra text'" />

which will concatenate the string onto the value when it changes.

v-bind: Shorthand

Use

<legend-component :title="legend + ' extra text'" />

which is an alias for v-bind.

Modular Styles

When including styles in a vue template, we can use the ‘module’ attribute to tell vue to suffix our CSS with a random string.

<style lang="scss" module>

This allows us to access modular styles in our template using the special $style variable which is made available. Use :class as a shortcut to the v-bind method.

The style object is a property of the Vue component. access

<div :class="$style.sidebar">

Class can also use an array of elements if other classes need to be used alongside the module generated class name. Use the following;

<div :class="[$style.sidebar, 'p-3', 'mb-5']">

Another trick of :class is the use of an object instead of an array. This allows us to use the classnames as keys, and set whether they should be active or not. To use dynamic keys, such as the $style.sidebar above, it needs to be wrapped in square brackets. Confusing as it looks like an array.

  <div :class="{ 
    [$style.component]: true, 
    'p-3': true, 
    'mb-5': true 
    }">

:global – don’t namespace a child class

When Vue generates module CSS classes for the template, it will also do so for child classes within our component. E.g.

<style lang="scss" module>
.component {

  ul {
    li.selected {
      background: $light-component-border;
     }
  }
}
</style>

Would produce classes such as 

```css
.sidebar_component_1vx3Z ul li.selected_2dxwF

Where the second hash is not required; the encapsulating hash is sufficient to namespace the class to the component. To exclude classes, add <code>:global</code> before the style declaration;

    :global li.selected { 

Which will now produce

.sidebar_component_1vx3Z ul li.selected

<h3>Using :global on the root component</h3>

Typically, using a namespaced root component is enough for our component; we don't need other classes to be hashed. To do this, so that we don't have to declare all child classes as <code>:global</code>, we can do the following;

.component :global {

### Module Classes & Dev Mode ###
Dynamically generated classnames can be awkward to debug in dev mode. Use this in <code>webpack.config.js</code> to

    .configureCssLoader((config) => {
        if (!Encore.isProduction() && config.modules) {
            config.modules.localIdentName = '[name]_[local]_[hash:base64:5]';
        }
    })
```

The [name] key is the filename of the component, E.g. products.vue would be 'products'. The [local] key is the classname, so 'sidebar' in this instance. Then we add a base64 hash with a length of ':5'.

This allows us to see what file a style exists in in dev mode.

Aliases

Aliases allow us to set base directories to use when importing files, this then negates the use of relative directory links. Adding the following to our webpack.config.js

.addAliases({
        '@': path.resolve(__dirname, 'assets', 'js'),
        styles: path.resolve(__dirname, 'assets', 'scss'),
    })

will allow us to use @ when importing javascript

import LegendComponent from '@/components/legend';

And ‘styles’ when importing styles into a javascript file;

import 'styles/app.scss';

However tilde must be prepended to the styles key when importing into a style tag;

@import "~styles/components/light-component";

Looping in Templates

Use the v-for attribute on the attribute which is to be looped over. This can be used for arrays and objects.

<li v-for="category in categories" class="nav-item">
    <a class="nav-link" :href="category.link">{{ category.name }}</a>
</li>

When using dynamic attributes like the href, remember to prepend with a colon to execute it as a javascript statement. The above template would render the following data;

    data() {
        return {
            categories: [{
                name: 'Horse Pringers',
                link: '#'
            }, {
                name: 'Amiga Hampers',
                link: '#'
            }]
        };
    },

This should however always be accompanied by a key attribute so that Vue can retain the element’s context and re-render it when required. VSCode will show an error when using the Vue extensions.

The v-for vue directive attribute should also be changed to include the key as the second parameter in the loop. The key attribute should also be prepended with a colon.

<li v-for="(category, index) in categories" :key="index" class="nav-item">

The key attribute does not render in the source code of the page as it’s a special attribute, which can be found here

Rendering an Element’s Contents – v-text

This can either be achieved using the curly brace syntax or using the v-text attribute, which is another vue directive. This can also contain standard javascript such as ternary conditionals;

<button class="btn btn-secondary btn-sm" 
   v-text="collapsed ? '>>' : '<< Collapse'">
</button>

Escaping

Vue automatically escapes HTML characters in strings when rendering in templates.

Events – v-on

Adding events to elements using the v-on directive. The shortcut for this is the @ symbol. Vue automatically prepends methods v-on on events with this., so the following actually becomes this.toggleCollapsed

<button class="btn btn-secondary btn-sm" 
   v-on:click="toggleCollapsed">
</button>

@ shortcut

<button class="btn btn-secondary btn-sm" 
      @click="toggleCollapsed">
</button>

append the regular javascript event after the colon. Target a method defined in the “`methods“ object in the component definition;

export default {
    name: 'Sidebar',
    data() {
        return {
            collapsed: false,
        };
    },
    methods : {
        toggleCollapsed() {
            this.collapsed = !this.collapsed;
        }
    }
}

Component Structure

Component properties which start with dollar are created internally by Vue, and can be manipulated if required for more advanced operations. Methods starting with underscore should be treated as private and not used.

These can however be useful when debugging and can be output in the component. E.g. every instantiated component is assigned a _uid which can be output in the template using

{{ _uid }}

It would appear that this was adopted to avoid conflicts with user created methods.

## Component instantiation – created() ##
When a component instantiates, its created method is automatically called if defined.

Conditionals – v-if and v-show Directives

A v-if on an element will completely remove it from the dom when the conditional doesn’t pass.

<div v-if="!collapsed">

A v-show conditional hides the element with display:none

<div v-show="!collapsed">

Showing should be used for simple elements which hide and appear frequently, and v-if should be used for more complex elements which reduces Vue’s rendering time when removed completely.

‘component’ root Style convention

It’s considered convention to call the root of a component’s style ‘component’. When configureCssLoader is used in development, the class name will be prepended with the name of the template file it exists in. Class names are only more cryptic in production.

Computed Properties

Computed properties allow us to take logic out of our template and into our component javascript. They don’t accept parameters, and behave just like a property on the component.

Instead of doing inline logic to switch classes for example;

  <div
    :class="{
      [$style.component]: true,
      [$style.collapsed]: collapsed,
      'p-3': true,
      'mb-5': true,
    }"
  >

We can instead do the following. Computed methods are called as though they are properties in templates javascript files.

<div :class="componentClass">

And then define our computed method in the component’s computed key;

  computed: {
    componentClass() {
      const classes = [this.$style.component, "p-3", "mb-5"];

      if(this.collapsed) {
        classes.push(this.$style.collapsed);
      }
 
      return classes;
    },

Note that classes are accessed through the $style object. This ensures we use the generated classes with transformations.

Computed & Ordering

For convention, the ‘computed’ key should be before the ‘methods’ key in a component definition.

### Performance ###
Computed methods will only run if a property referenced in the method changes on the object. So, in the above example, if we had a button which called

methods: {
    toggleCollapsed() {
      this.collapsed = !this.collapsed;
    },
},

then componentClass would update the class on the element. If we were to remove the line which modifies the collapsed property, the componentClass function would not execute!

Shared CSS

Normal shared styles should be imported and used with components where appropriate, and used as regular strings rather than using the $style object.

This will work for global styles, but for shared styles which are imported into a component, by default they may be duplicated with the module’s prefix.

To get around this, we can add a second style tag, with a lang of “css”. SCSS can be imported into this style tag, but using “scss” as the lang attribute “for some reason” duplicates the styles.

<style lang="css>
@import '~path/to/style.scss'
</style>

AJAX With Axios

yarn add axios --dev

This is an alternative to the browser's built in 'fetch' method, however this is not supported in IE 11.

Hook Methods

We can use the methods mounted and created to perform actions when we use a component. Mounted when it’s actually added to the page.

To call an ajax endpoint when a component mounts;

  mounted() {
    axios.get('/api/products').then((response) => {
        // Do things.
    });
  }

Use of created

The created hook can often be the better hook for performing ajax requests as it will start as soon as the component is created rather than added to the page.

  created() {
    axios.get('/api/products').then((response) => {
        // Do things.
    });
  }

await & async

This waits for an AJAX request to complete before continuing code execution. It’s also legal to use await on function which do not return promises, they’re just executed and returned instantly. To use this, the encapsulated method must have the async keyword before it;

async mounted() {
    const response = await axios.get('/api/products');
  }

When an async method is defined, it will always return a Promise. Only the code in the async method will pause whilst an await call is made, the rest of the application code will continue execution. The returned promise can be used to chain other operations which may require the data returned in the async method.

export default {
  name: "Catalog", 
  data: () => ({
    products: []
  }),
  async mounted() {
    const response = await axios.get('/api/products');
    this.products = response.data['hydra:member'];
  }
};

## API Platform ##
When using <a href="https://api-platform.com/">API Platform</a>, accessing the API in a browser will give the GUI. It does this by checking the ```accept``` header. To access endpoints which AJAX calls directly, add ```.jsonld``` to the end of the url.

### @id and API Platform ###
Instead of the traditional use of primary key database ids when assigning keys to objects (mainly in loops), the ```@id``` attribute can also be used, which contains the unique URI for the item in question. This can be more useful than just using the ```id``` primary key. This is known as the IRI.

## Smart Component, Dumb Component ##
The logical hierarchy of components should be defined by their usage. Creating a product list component could load the products and render them, however defining its data as a prop which is set by a "smart" component can be preferable.

A smart component can be considered like a Controller, and dumb components akin to templates.

### catalog.vue ###

<template>
<div>
<div class="row">
<div class="col-12">
<h1>Products</h1>
</div>
</div>

&lt;product-list :products=&quot;products&quot; /&gt;

</div>
</template>

<script>
import axios from 'axios';
import ProductList from '@/components/product-list';

export default {
name: "Catalog",
components: {
ProductList
},
data: () => ({
products: [],
}),
async mounted() {
const response = await axios.get('/api/products');
this.products = response.data['hydra:member'];
}
};
</script>

<br />### product-list/index.vue ###
<template>
  <div class="row">
    <div
      v-for="product in products"
      :key="product['id']"
      class="col-xs-12 col-6 mb-2 pb-2"
    >
      {{ product.name }}
    </div>
  </div>
</template>

<script>
export default {
    name: 'ProductList',
    props: {
        products: {
            type: Array,
            required: trueprodu
        }
    },
    data: {
        products: []
    }
};
</script>

In this example, the catalog component is responsible for providing the data for the product-list component. The import for the product list does not need to include the ‘index’ part of the file path. This is assumed by Vue when we import a directory.

Passing Server Data to Vue

Access the window object in computed return methods to initialise the Vue app with data on page load. Whilst this is legal, it often gets messy so it’s better to use something like JS Services to centralise the data.

Initializing Vuw with data on the page is a preferred approach to stop the initial load ‘pop’ when ajaxing in data.

Services

It’s often better to split functionality up into smaller pieces, rather than have it all lumped together in the component’s template file. We can create smaller service methods to achieve this, so that things like AJAX requests are separated from our component’s logic.

Vue Lifecycle

Taken from here