Skip to content

Instantly share code, notes, and snippets.

@lbogdan
Last active January 22, 2019 20:25
Show Gist options
  • Save lbogdan/9eaff29c07845c014fffc0657b68e218 to your computer and use it in GitHub Desktop.
Save lbogdan/9eaff29c07845c014fffc0657b68e218 to your computer and use it in GitHub Desktop.
[WIP] Intro to Vue.js Workshop

At the beginning of the workshop you should have all of the following installed:

As a bonus, please try to read the following article: https://overreacted.io/the-elements-of-ui-engineering/ .

1. To get started, a bit of content (+)

  • change App <template> to contain:
    • a <h1> containing the text "Star Wars: The Last Jedi (2017)"
    • a <h2> containing the text "Action, Adventure, Fantasy"
    • a <p> containing the text "Rey develops her newly discovered abilities with the guidance of Luke Skywalker, who is unsettled by the strength of her powers. Meanwhile, the Resistance prepares to do battle with the First Order."
    • an <img> with src set to "https://images-na.ssl-images-amazon.com/images/I/51ih4cPagFL.jpg" and alt set to "Movie poster"

2. Extracting state: v-bind (++)

  • in App, define a data() function to return and object having the following properties: title, year, genre, plot, poster
// App.vue
export default {
  name: 'app',
  data() {
    return {
      title: 'Star Wars: The Last Jedi',
      year: 2017,
      ...
    }
  }
}
  • in App, replace static content from the <template> using state; use {{ }} interpolation for text and binding for <image> src
<!-- App.vue -->
<template>
  <div id="app">
    <h1>{{ title }}</h1>
    <!--...-->
    <img v-bind:src="poster" alt="Movie poster" />
  </div>
</template>

3. Extracting component: component import & registration (+++)

  • create a new file src/components/MovieItem.vue
  • move template and state from App to MovieItem, remove id from root <div>, change name to "movie-item"
  • register MovieItem inside App <script>:
// App.vue
import MovieItem from `./components/MovieItem.vue';

export default {
  ...
  components: {
    MovieItem
  }
}
  • render it in App <template>:
<!-- App.vue -->
<movie-item />

4. Making things pretty (++)

  • add bootstrap package: yarn add --dev bootstrap
  • import the bootstrap css in main.js import 'bootstrap/dist/css/bootstrap.css'
  • add "container" class to App <div>
  • change MovieItem <template> to use the following markup:
<!-- MovieItem.vue -->
<div class="card mt-4">
  <div class="card-body">
    <h5 class="card-title">{{ title }} <small>({{ year }})</small></h5>
    <h6 class="card-subtitle text-muted">{{ genre }}</h6>
    <hr />
    <div class="row">
      <div class="col-sm-4">
        <img :src="poster" class="img-fluid" alt="Movie poster" />
      </div>
      <div class="col-sm-8"><p>{{ plot }}</p></div>
    </div>
  </div>
</div>

5. Editing comments: v-model on DOM input elements & v-on (+++)

  • add a comment <textarea> and a Clear comment button to MovieItem, using the following markup:
<!-- MovieItem.vue -->
<div class="row mt-4">
  <div class="col-md-8 form-group mb-0">
    <textarea
      class="form-control"
      rows="3"
      placeholder="Did you like this movie?"
    ></textarea>
    <button class="btn btn-primary mt-2">Clear comment</button>
  </div>
</div>
  • add a new data property named comment and initialize it to an empty string
  • 2-way bind it to the <textarea>:
<!-- MovieItem.vue -->
<textarea v-model="comment"></textarea>
  • disable the <button> when the comment is empty:
<!-- MovieItem.vue -->
<button :disabled="comment.length === 0">Clear message</button>
  • clear the comment when clicking the <button>
<!-- MovieItem.vue -->
<button ... v-on:click="comment = ''"></button>

6. Counting words: methods (+++)

  • add a method to MovieItem methods named wordCount, that receives a string argument and returns how many words it contains
// MovieItem.vue
export default {
  ...
  methods: {
    wordCount(str) {
      // count logic here
    }
  }
}

hint: use Array.split() to split the string into words and Array.filter() to filter empty words

  • add a div under <textarea>, and using {{ }} interpolation, call the above method with comment as argument; add " words" suffix
  • use interpolation to show "word" or "words" depending on the number of words

7. Style binding: :style (++)

  • make the word count <div> red when the count is 0, green otherwise
    • use a style binding to set the CSS color property; use "limegreen" and "salmon" for colors
<!-- MovieItem.vue -->
<div :style="{ color: condition ? 'limegreen' : 'salmon' }">...</div>

bonus: debug how many times the wordCount() method is called

8. Counting words, take two: computeds (++)

  • change the wordCount method into a commentWordCount computed
  • it should receive no arguments, and work on this.comment directly

bonus: debug how many times the computed method commentWordCount() method is called

9. Class binding: :class (+)

  • refactor word count color to use classes instead of styles; use "text-danger" and "text-success" classes
<!-- MovieItem.vue -->
<div class="[ condition ? 'text-success' : 'text-danger' ]">...</div>

10. A bit of refactoring (++)

  • in MovieItem, move all data properties into an item object
  • fix all references to movie properties throughout the component

11 Moving state to the parent: props (+++)

  • in App data(), define a "movie" object and copy the properties from MovieItem
  • in MovieItem, remove item from data(), and define it as a prop instead:
// MovieItem.vue
export default {
  ...
  props: {
    item: {
      type: String,
      required: true
    }
  }
}
  • in App, bind "item" to "movie" on the <movie-item> element
<!-- App.vue -->
<movie-item :item="movie" />
  • in App data(), add another object "movie2" with the following properties:
// App.vue
{
  title: 'Black Swan',
  year: 2010,
  genre: 'Drama, Thriller',
  plot: 'A committed dancer wins the lead role in a production of Tchaikovskys \"Swan Lake\" only to find herself struggling to maintain her sanity.',
  poster: 'https://images-na.ssl-images-amazon.com/images/I/615yWgAir2L._SY500_.jpg'
}
  • in App <template>, add another <movie-item> bound to "movie2"
  • check the console for errors, you'll have a few surprises! ;-)
  • add class "mb-2" to App <div>

12. A bit of responsivness (+)

  • display 2 cards per row on large screens:
    • in App, wrap the movie cards in a <div class="row">
    • in MovieItem <template>, wrap everything with a <div class="col-lg-6 px-2> div

13. Conditional rendering: v-if & v-else (+++)

  • in MovieItem, add a data() property named "isEditing", initially set to false
  • wrap the <textarea> and the word count <div> with a <template v-if="isEditing">
  • add a <div v-else> immediately after the above <template> and show the comment prefixed by "Your comment: "
  • if the comment is empty, display "No comment yet."
  • change the "Clear comment" button's text to "Edit/Save comment", remove its old behavior and make it toggle the "isEditing" state

14. HTML rendering: v-html (++)

  • define a "formattedComment" computed that returns "No comment yet." if the comment is empty, and "Your comment: <b>...</b>" otherwise
  • set the previous <div v-else> content to the computed:
<!-- MovieItem.vue -->
<div v-else v-html="formattedContent"></div>

15. Extracting MovieComment component (++++)

  • extract the parts of MovieItem <template> and <script> that have to do with showing and editing of the comment to a new MovieComment component

hint: create the new MovieComment component, move the comment part of the MovieItem <template> to it, import, register and render a <movie-comment /> in place of the moved layout, and try to move / fix things around until you get rid of all the console errors and the previous functionality is restored

  • in MovieComment, define a "comment" prop, bind it from MovieItem
  • try to edit the comment and check the console for errors ;-)

15.1. Refactor MovieComment to handle v-model: v-model on Vue components & watchers (++++)

hint: <movie-comment v-model="item.comment"> is shorthand for <movie-comment :value="item.comment" @input=item.comment = $event">, where $event is a special Vue property set to the input event's payload

  • in MovieComment, define "value" prop, move the "comment" prop to data() and set its initial value to the "value" prop
// MovieComment.vue
export default {
  ...
  data() {
    return {
      ...
      comment: this.value
    }
  },
  props: {
    value: {
      type: String,
      required: true
    }
  }
}
  • in MovieComment, create an editOrSave() method which on save will emit an input event having the new comment value as payload:
// MovieComment.vue
export default {
  ...
  methods: {
    editOrSave() {
      if (this.isEditing) {
        this.$emit('input', /* payload */, this.comment);
      }
    }
  }
}
  • set the above method as the <button>'s @click handler
  • in MovieItem <template>, replace the :comment binding to a v-model one
  • try to change the comment from Vue devtools

    hint: it won't work, because comment is initialized to v-model's value only when MovieComment is created (when data() is called)

  • to fix this, add a watcher to update comment when value changes:
// MovieComment
export default {
  ...
  watch: {
    value(newValue) {
      this.comment = newValue;
    }
  }
}
  • add a "Cancel" button

16. List rendering: v-for (++)

  • in App data(), replace "movie" and "movie2" with a "movies" array having the following content:
// App.vue
export default {
  ...
  data() {
    return {
      movies: [
        {
          id: 1,
          title: 'Star Wars: The Last Jedi',
          year: 2017,
          genre: 'Action, Adventure, Fantasy',
          plot:
            'Rey develops her newly discovered abilities with the guidance of Luke Skywalker, who is unsettled by the strength of her powers. Meanwhile, the Resistance prepares to do battle with the First Order.',
          poster:
            'https://images-na.ssl-images-amazon.com/images/I/51ih4cPagFL.jpg',
          comment: '',
        },
        {
          id: 2,
          title: 'Black Swan',
          year: 2010,
          genre: 'Drama, Thriller',
          plot:
            'A committed dancer wins the lead role in a production of Tchaikovskys "Swan Lake" only to find herself struggling to maintain her sanity.',
          poster:
            'https://images-na.ssl-images-amazon.com/images/I/615yWgAir2L._SY500_.jpg',
          comment: '',
        },
        {
          id: 3,
          title: 'Fight Club',
          year: 1999,
          genre: 'Drama',
          plot:
            'An insomniac office worker, looking for a way to change his life, crosses paths with a devil-may-care soapmaker, forming an underground fight club that evolves into something much, much more.',
          poster:
            'https://images-na.ssl-images-amazon.com/images/I/51iOANjtCQL.jpg',
          comment: '',
        },
        {
          id: 4,
          title: 'The Godfather: Part II',
          year: 1974,
          genre: 'Crime, Drama',
          plot:
            'The early life and career of Vito Corleone in 1920s New York City is portrayed, while his son, Michael, expands and tightens his grip on the family crime syndicate.',
          poster:
            'https://images-na.ssl-images-amazon.com/images/I/41V2AB34KCL.jpg',
          comment: '',
        },
      ],
    }
  }
}
  • in App <template>, replace the two static <movie-item>s with a dynamic <movie-item> list, iterating over movies:
<!-- App.vue -->
<movie-list v-for="movie in movies" :item="movie" :key="movie.id" />
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment