Lab 13: Vue Basics
Due Date: June 04
Objectives
- introduce students to basic concepts in Vue.js
- teach students to build better code through components
- help students integrate Vue.js into a Rails app
Due Date: June 04
Objectives
We are going to warm up by doing some simple data binding with Vue.js. To start, get the HTML starter file on Github. You should also install the Vuejs developer tool for Chrome from the Chrome Web Store. To use this today, you will need to check "Allow access to file URLs" for this extension in Chrome's extension management panel (you can access this by right-clicking or control-clicking the extension in the top-right, and clicking "Manage Extensions").
This starter page has a very basic Vue instance set up and connected to the div with an ID of #main
. Inside that instance, right below the el
declaration, add in some simple data:
data: {
message: 'Hello World'
}
Be sure to add a comma after el: '#main'
as well. Then add a simple text box input inside the #main
div (<input type="text">
). Open up this page in a browser to see nothing special yet.
To do the most basic data binding, let's edit the input
tag to also have the property v-model="message"
. After refreshing this page, you will see that the value of message is displayed in the box. Open up the VueJS dev tool so you see the data value clearly displayed. In the text box, replace the contents with the more appropriate "nuqneH Terran" (which everyone knows is just 'Hello World' in Klingon) and see that as you update the contents, the message in data is changing. Likewise, use the Vuejs dev tool to edit the message and see that the text box updates automatically because of the data binding.
We want to display the contents of the data message on the page itself, so we can see the changes without using the dev tools. To make this happen, below the text box, add the following:
<p>The value of the input is: {{ message }}</p>
Now modify the text box and see that the message displayed is automatically changing. Amazing.
To verify that this data binding is only happening to the materials within the #main
div, add the same text box with the same v-model
binding below the message "Nothing happening outside of main div..." and see that no data binding is happening there.
In class we extended this further with lists and used other Vue directives such as v-for
and v-on
and you should feel free to play with these more on your own, but for now we are going to move onto the main part of this lab: building better tabs with Vue.js
In class we mentioned that Vue.js components are useful because they give us some interactivity, but also because they allow us to clean up a code into something more understandable and maintainable. A simple example of clutter that comes with simple html code can be seen with tabs. A tabbed interface is often very useful, but the markup is cluttered with lots of nested content and the page becomes hard to read and wade through. We are going to create a tab Vue component that will clean up our markup considerably.
Go to the documentation on tabs in MaterializeCSS and refresh yourself on how tabs basically work (note we are using Materialize 0.100.2 documentation). On the same page there is the markup for the tabs themselves (using ul tags) and right underneath some div tags that hold the content for the tabs. The div tags in the demo are super simple, but this can often be a long and hard to read page.
You should also have the starter code for the tab demo in the folder you cloned from git earlier. Look over the code and see how we are adding in jquery, materialize (both css and js), and VueJS, as well as setting up the initialization javascript that Materialize requires to make its components work.
The way we'd like these tabs to work is instead of declaring the tabs in one place and then going somewhere else of the content, that we would would just have a tabs
component that would be made up of a set of tab
components. Each tab
component would have its content between the <tab></tab>
tags so there is no mapping back and forth between the tab and its content. Below is just a sample of what it might look like -- don't use this per se (no copy/paste here) but looking at it as a guide, create three tabs and some related content that you find interesting.
<tabs>
<tab name="about us" v-bind:selected="true">
<h5>The content of the About Us tab</h5>
<p>I love Vue.js.</p>
</tab>
<tab name="contact">
<h5>The content of the Contact tab</h5>
<p>I love clean, refactored code better.</p>
</tab>
<tab name="privacy">
<h5>The content of the Privacy tab</h5>
<p>All the more reason you'll love Vue.js.</p>
</tab>
</tabs>
Place your own set of tabs in the div#main
section where the comments indicate tabs go.
div#main
and create the appropriate tabs for us using Materialize. To start, you will first create a main.js
file in the same directory as the tab_demo.html
file and start with the following skeletal code:Vue.component('tab', {
template: `
`,
props: {
},
data() {
},
computed: {
},
mounted() {
}
});
Vue.component('tabs', {
template: `
`,
data() {
},
created() {
},
methods: {
}
});
var vm = new Vue({
el: '#main'
});
This gives us the bare bones to build both a single tab component as well as tabs component, which is the set of tabs to be displayed. To connect this main.js
to the demo file, add at the end of the demo, between the #main
div and the container div, the following:
<script type="text/javascript" src="main.js"></script>
tab
component and the first thing we need is a template. This template is pretty easy -- we will just have a div with a slot for content; whatever content we have between the <tab>
tags will get displayed in this slot. Of course, we only want this tab component to be displayed if it's the active tab, so we'll use the Vue directive v-show
to only show it if it's active. Our template will look like this, which we will drop into the template
field in the tab
component:<div v-show="isActive"><slot></slot></div>
props
. The first is a name, which will be required (a blank tab would be confusing), and the selected property, which starts off as false for all tabs. Adding the following code to props
will do the trick:name: { required: true },
selected: { default: false }
data()
, we simply want to return (for now) isActive
and have that set to false. Add the following to data()
:return {
isActive: false
};
href=#...
that connects the tab with its content below. We need to generate these based off the tab name. We also need to be careful that we replace any spaces with dashes as links can't have spaces in them so we will use the replace function and a simple regex to handle that. The following should be added to computed:href() {
return "#" + this.name.toLowerCase().replace(/ /g, '-');
}
Finally, once the components are mounted, we need a way to find the tab marked selected
and set it to active. In the mounted()
method we can write the following: this.isActive = this.selected;
We have a tab
component but now need a tabs
component to collect all the tabs and allow interactivity between them. We will start with building the template for tabs
. Go to MaterializeCSS and copy their demo code in the language-markup
box at the top of the tabs page from before and paste in into the template. We will use this shell, but now need to generalize it. First off, inside the ul
tags, get rid of all the li
tags except the one associated with 'Test 2'. Now within the remaining li
tag, add in the vue directive v-for="tab in tabs"
so we can create as many tabs as we have in a tabs array (to be created shortly). Within the a
tag nested in the li
, change the href to v-bind:href="tab.href"
so we can later find the proper href for the tag. Also realizing that the active tab is determined by the presence of a class called 'active', we can replace that with the directive v-bind:class="{ 'active': tab.isActive }"
. Finally, to make sure that the tabs change over, we will add in v-on:click="selectTab(tab)"
to the a
tag. Between the opening and closing a
tags, we simply want to display the name of the tab with {{ tab.name }}
Similarly, among the divs that display content, let's remove all divs except the one related to 'Test 2'. Replace all the div properties with just one: class="tab-details"
. The actual content (just the "Test 2" statement in this case) replace with <slot></slot>
tags. Because Vue.js wants a single div tag at the root (and we have two -- one associated with the tabs and the other with the tab content), let's wrap this entire thing is another plain div
tag.
In the data() section of our tabs
component, we want to be able to get access to our tabs array, which for now is empty, and we can do that with: return { tabs: [] };
Now we have a tabs array, but we need to populate it with actual tab components. We will use the created
lifecycle hook so that we populate the tabs array with the tabs that are on the html page. (Remember that between the tabs
tags, each of the tab
tags is a child element to tabs
. Therefore, to find each tab
component and add it to our tabs array, we can add to created()
the following: this.tabs = this.$children;
As noted earlier, Materialize identifies the active tab by the html class active
. We need a method that will find this tab component for us. The code below provides such a method:
selectTab(selectedTab) {
this.tabs.forEach(tab => {
tab.isActive = (tab.name == selectedTab.name)
});
}
main.js
file is complete and it is connected to the tab_demo.html
file, we can simply open (or reload) the tab demo and see that everything is working as expected. Even better, our markdown for the tabs is simple and intuitive and much more maintainable in the future.Show a TA that you have the Vue tabs set up and working. Make sure the TA initials your sheet.
This is great, but as we saw in class, the real power of this is adding it to an existing Rails app and cleaning up the code with partials. Since Prof. H hasn't posted the code for his demo, we will just recreate it here. Assuming you have cloned PATS_v2, go to that project on the command line and create a new branch called 'tabs' and then open the project in your editor. (As we recall, we shouldn't do most of our dev work in the master branch.)
First step is easy -- we will use the vuejs-rails gem to incorporate Vue.js into PATS. Simply add gem 'vuejs-rails'
to the Gemfile and user bundler to install the gem.
Now that we have that, go to app/assets/javascripts/application.js
and after the line requiring materialize-form, add a line to require vue
.
Now within the same directory (app/assets/javascripts/
) add a file called vue_tab_components.js
. This file will be added to our project automatically by the line require_tree .
in application.js
. We want to paste our code from main.js
from part 2 of this lab with all of the tab components, but one small change is that we will need to wrap this content in a $(document).on('ready'...) so it only runs after the page has been loaded. Your file will look like the following:
$(document).on('ready', function(){
// Add code from main.js from lab part 2 here!
});
One last small change to make is that we will change the el: '#main'
to el: '#tabbed'
so that the Vue will mount all this within a div#tabbed
rather than div#main
.
Now within the the views for pets, add a new partial called _tabbed_index.html.erb
. In that partial add the following:
<div id="tabbed">
<tabs>
<tab name="active pets" v-bind:selected="true">
<%= render :partial => "pet_list", locals: {pets: @active_pets, state: 'active'} %>
</tab>
<tab name="inactive pets">
<%= render :partial => "pet_list", locals: {pets: @inactive_pets, state: 'inactive'} %>
</tab>
</tabs>
</div>
<p> </p>
<%= render :partial => "partials/add_new_object", locals: {model_name: 'pet'} %>
This will create an index page with two tabs, one for active pets (which is initially selected) and another for inactive pets. This page is easy to read and understand.
Now go to the index.html.erb
view in pets and replace the primary partial of "active_pets" with your new partial "tabbed_index". Also set the sidebar to nil
as we are now handling this with tabs. Start up rails server and see your handiwork in action.
Oh dear -- that didn't go as expected. What's the problem here? Stop and try to figure it out now before going onto the next step. (Taking the time to figure it out will help you learn more than if you just jump immediately to the answer below.)
The issue is we don't have a _pet_list.html.erb
partial that is being referenced in the tabbed_index partial (twice), but we do have an 'active_pets' one. Let's generalize that one so it works for both active and inactive pets. Start by creating the _pet_list.html.erb
partial with following starter code:
<h4><%= state.capitalize %> Pets at PATS</h4>
<% if pets.empty? %>
<!-- a message about no pets in this state (and be specific about the state) goes here -->
<% else %>
<!-- data table goes here -->
<% end %>
Using the active_pets partial as a guide, you can finish it up from here. One reminder though -- as you make this generalized partial, also be sure to change the controller action so that both active and inactive pets are paginated (else you will get another error). Once done, reload the page and see your tabbed interface at work in PATS.
Show a TA that you have applied the Vue tabs to the PATS project and it is working. Make sure the TA initials your sheet.