1905050
The next assignment I set for myself was to enhance the Todo app, working with my plain HTML/JavaScript version. This has been accomplished between April 28 and today, May 5. A compiled version of the original Todo app at the end of the Traversy course can be seen here. To this I implemented the following enhancements, which are discussed in more detail hereafter. The image above shows the end result on a mobile phone, and it can also be seen in action here.
- Data Source dropdown with Memory, HTTP and SQLite options.
- Disabling adding of items when HTTP website or SQLite data not available.
- Saving the Memory list between data source changes.
- Saving the last data source selection as a cookie, and fetching this value when the app starts.
- Adding list of enhancements to the About page.
1. Data Source dropdown with Memory, HTTP and SQLite options
The Memory option would deal with data in an array held in memory, like was done in the initial Traversy course app. HTTP would handle dummy data from the jsonplaceholder website, as later introduced in the Traversy course app. And SQLite would handle data in a real SQLite database, using a PHP script I created weeks earlier for the React course app implementation. Implementing the dropdown required some research and investigation, as I needed to address several things not covered in the course videos I’d seen: Defining the Vue instance options correctly including the router and other attributes, how to pass parameters to the route components and ensuring the Home component was re-rendered when the data source changed.
1.1. Defining the Vue instance with router and other attributes
After the router was added to the Traversy course app via the Vue Package Manager GUI, the Vue instance declaration in the src/main.js file was
new Vue({router, render: h => h(App) }).$mount('#app')
and the declaration presented in the example code for the Vue-router documentation which I started using as a basis, was
new Vue({ router }).$mount('#app')
while previously, the instance definition I was used to from the earlier tutorials was something like
new Vue({ el: '#app', data: {message: 'Hello World!'}})
I needed to understand why introducing the router changed the syntax. I read about the render attribute, wondering why it was used in one syntax and not in another, and then the $mount() method. It was when I saw that attaching .$mount('#app') was effectively the same as specifying el: '#app' for the options that I got rid of it. The Vue options that was specified as simply { router } in the second syntax was a shorthand for { router: router } and I could continue expanding by adding more attributes to it, so that it eventually became something like this by the time I was done:
new Vue({
router,
el: '#app',
data: {...},
watch: {...},
created(){ ... },
methods: {...}
}) 1.2. Passing parameters to a route component
The application now had two route components Home and About assigned to the router object. Specifying appname.html#/ in the URL would activate the Home component while #/about would show the other. Home is the one that deals with todo data, and what it displayed should depend on the data source selected. The Vue instance declaration earlier above would create the root component named app, making three. So first question I needed to resolve was in which of the components should the data source entry be created, followed by how should it be supplied to the Home component.
Since the data source selection should be done before the data got displayed, I placed the <select> for it in the root, so declared the datasource variable in its data attribute. The documentation about passing parameters to a route component talked about passing parameters via the URL, eg specifying a route path of '/user/:id' would make id a route parameter which can be referenced as {{ $route.params.id }} but how should the parameter be passed from within the application? After some time I saw that, where the routes were declared, you can indicate props:true for the component that required parameters. Then in the component’s definition, you specify the props attribute, an array listing the names of the parameters expected. With this done, {{ $route.params.id }} could then be replaced with {{ id }}. The remaining question was which of the HTML elements for the route component should the parameter be supplied from? Was it <router-link> or <router-view>? By trial and error, I eventually found the answer to be <router-view>.
1.3. Ensuring the Home component was re-rendered when the data source changed
Now my <select> was up there next to the two links for the Home and About components, and below it the todo list was displayed for the memory-based array. On selecting the HTTP or SQLite data sources though, nothing happened. My next challenge became how to force the Home component to re-render itself once the data source changed. Investigating things with the browser console, I could see that the value of app.datasource changed with the <select> value, but further down, the datasource in the props for the Home component being rendered in the <router-view> remained undefined. As I pondered and tinkered along, I read about the $forceUpdate() method and when I issued it at the console, the datasource value for the props got updated. So I started looking for the right place to position the $forceUpdate() to get the re-rendering done. Unable to do so, I went looking for an answer on the web. A blog post by Michael Thiessen found from my queries, The correct way to force Vue to re-render a component answered the question for me. He said the best way was to bind a unique key for the component. I already had the datasource variable bound to the <router-view> so next I added :key="datasource" and this did the magic! Changing the data source promptly re-rendered the component with its rightful values.
2. Disabling adding of items when data source not available
Displaying existing todo list is done in the created() hook for the Home component, where axios() calls are used to retrieve data either from the jsonplaceholder website or the PHP script for SQLite. Disabling adding of items is done in the catch() method for the axios() calls, calling the disableAdd() component method. This method sets the dataFailed variable to true, displays message showing that the data source is not available, and then disables the two DOM elements in the AddTodo component, with id values of addtext and addbtn. DataFailed is passed on to the Todo component to control displaying of both the checkmark and the delete button for the item.
3. Saving the Memory list between data source changes
The Home component has two arrays todos (current todo list regardless of data source value) and todosmem (memory todo list initially hard-coded with three items). When a different data source is selected, the Home component gets re-rendered. This means only the hard-coded values in todosmem will show, unless this array is saved between changes of data source.
To maintain the values in the todosmem array between data source changes, it has to be declared at a higher-level component, no longer in Home. I moved it to the root instance (app) and at first passed it to Home as a props value. It got displayed like before. Then I introduced a beforeDestroy() lifecycle hook for the Home component, assigning todos to todosmem if the Memory data source was selected. This didn’t work, as Vue gave me the following warning:
[Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "todosmem"
found in
---> <Anonymous>
<Root> vue.js:634:9
Something like that had worked in the React version of the app, meaning Vue was more strict. <Anonymous> in the message meant the <router-view> component. So next I issued a "backup" event instead, passing the todos array as argument. In the root app component where todosmem was still declared, I introduced backupTodos() instance method, called from
<router-view :datasource="datasource" @backup="backupTodos" ... />
that assigned the argument it was given to the todosmem array. This was what worked.
4. Saving and retrieving the last data source via cookie
Using the v-model directive on the <select> element in the HTML file, the selected dropdown value for data source was assigned to the Vue variable datasource automatically. I introduced a watch property for the root component, for datasource and wrote the associated watch function that will be triggered when the value changed, to store the value as a cookie. Retrieving the last-saved cookie was done in the created() lifecycle hook for the root component.
For a long time though the statement to save the cookie, something like
document.cookie = 'datasource='+c
didn’t seem to work. Trouble-shooting at the browser console, each time I changed the data source, I would type my getDatasource() function and it would reply with an empty string. I couldn’t understand it. It was a hunch that solved the riddle. I had not started the Xampp’s Apache web server service and the HTML file was opened with the file:// protocol instead of http://. Maybe it was like PHP code that also may not work without a web server. I went and started the web service, and voila! The cookie now got saved.
5. Adding list of enhancements to the About page
When I expanded the template HTML for the About component by adding <ol> and <li> items, I expected to see a numbered list but none was produced when the component was rendered. I tried <ul> with the same result. Was it that Vue didn’t produce the standard list items, that it removed the list formatting? I tried assigning a style='list-style-type: square;' directly to the <ul> with no change. Later I copied out the <li> values into a data() function for the component and used v-for. Still the same, so it wasn’t an issue with the list tags. It was after I introduced list-style-position: inside; to the CSS formatting for the <ul> that I began to see a change. After tinkering with left margin, left padding and text-indent settings, I eventually realized that it was the absence of both padding and margin—set to zero for all elements at the top of the CSS file—that caused the list styles to not show up initially. With list-style-position removed I no longer had to use a negative text-indent to get the right first-line indentation.
Still using the data() property, I marked up some of the text in the array to be displayed by the v-for directive. The result was that the mark-ups like <em> came out like normal text rather than being applied. So I moved the items back to the template property using <ol> and <li> elements. Once the formatting was applied after refreshing the page, I then got rid of the data() property for the component.