5
\$\begingroup\$

I'm currently trying to teach myself Vue.js and I've just created my first small project: a simple todo list. While writing the component I discovered that you can't access methods from the parent component directly, but I didn't want to put one half of the code in the child component (the deleteToDo method), so I've passed a callback to it. Is this okay in Vue or is there any other preferred way of structuring things like this?

JSFiddle

Vue.component('todo-item', {
    props: ['todo', 'delete_callback'],
    template: `<li>
                    <input type="checkbox" v-model="todo.done">
                    <input v-model="todo.text" :disabled="todo.done" @change="todoChanged(todo)">
                    <a @click="delete_callback(todo)">X</a>
                </li>`,
    methods: {
        todoChanged: function (todo) {
            if (this.todo.text == '') {
                this.delete_callback(todo);
            }
        }
    }
});

var vm = new Vue({
    el: '#app',
    data: {
        show: {
            todo: true,
            done: false
        },
        todos: [
            {
                id: 0,
                text: 'Test this stuff',
                done: true
            },
            {
                id: 1,
                text: 'Learn more Vue',
                done: false
            },
            {
                id: 2,
                text: 'Buy some tasty food',
                done: false
            }
        ]
    },
    methods: {
        addTodo: function (event) {
            if (event instanceof KeyboardEvent && event.key != 'Enter') {
                return;
            }
            if (event.target.value != '') {
                this.todos.push({
                    id: this.todos.length,
                    text: event.target.value,
                    done: false
                });
                event.target.value = '';
            }
        },
        deleteToDo: function (todo) {
            this.todos.splice(todo.id, 1);
            for (let i = 0; i < this.todos.length; i++) {
                this.todos[i].id = i;
            }
        }
    }
})
* {
    box-sizing: border-box;
}

body {
    font: 16px "Helvetica Neue", status-bar;
    background-color: #444444;
}

#app {
    max-width: 400px;
    margin: 3rem auto;
    padding: 1rem 3rem 3rem 3rem;
    background-color: #dddddd;
    color: #444444;
}

li {
    list-style: none;
}

li > input:disabled {
    text-decoration: line-through;
}

li > a, h4 > span {
    font-size: 0.7rem;
}

li > a:hover {
    cursor: pointer;
    color: #222222;
}

#newtodo {
    margin-left: 23px;
}
<!DOCTYPE html>
<html lang="en" dir="ltr">
    <head>
        <meta charset="utf-8">
        <title>ToDo</title>
        <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
        <link rel="stylesheet" href="test.css">
    </head>
    <body>
        <div id="app">
            <h1>ToDo List</h1>
            <h4 @click="show.todo = !show.todo">To Do: {{ todos.filter(todo => !todo.done).length }} <span>({{ show.todo ? 'Hide' : 'Show' }})</span></h4>
            <ul v-show="show.todo">
                <todo-item v-for="todo in todos" v-if="!todo.done" :todo="todo" :key="todo.id" :delete_callback="deleteToDo"></todo-item>
                <input id="newtodo" type="text" placeholder="New todo item" @keypress="addTodo" @blur="addTodo"></input>
            </ul>
            <h4 @click="show.done = !show.done">Done: {{ todos.filter(todo => todo.done).length }} <span>({{ show.done ? 'Hide' : 'Show' }})</span></h4>
            <ul v-show="show.done">
                <todo-item v-for="todo in todos" v-if="todo.done" :todo="todo" :key="todo.id" :delete_callback="deleteToDo"></todo-item>
            </ul>
        </div>
        <script src="test.js"></script>
    </body>
</html>

\$\endgroup\$
1
  • \$\begingroup\$ Thanks @SᴀᴍOnᴇᴌᴀ! I didn't know about this feature yet. I've included both the HTML and CSS snippets as well now. \$\endgroup\$ Commented Feb 13, 2019 at 17:46

1 Answer 1

4
\$\begingroup\$

Your question

so I've passed a callback to it. Is this okay in Vue or is there any other preferred way of structuring things like this?

If you haven't already, I would suggest you read the VueJS documentation for Components - specifically the section Listening to Child Components Events. Basically the child component can utilize the built-in $emit method to emit an event, which can be handled by the parent using v-on.

BTW the last section of the Documentation page List rendering has a section v-for with a Component that includes a simple TODO list using $emit.


Other feedback

You might want to consider using a key that is the index of each item - see the second example of Mapping an Array to Elements with v-for of the documentation:

<todo-item v-for="(todo, index) in todos" v-if="todo.done" :todo="todo" :key="index"

That way you don't have to re-assign the id values when deleting an item.


The Vue object is assigned to a variable vm

var vm = new Vue({

but it is never used after that. According to ESLint: "Such variables take up space in the code and can lead to confusion by readers."1.


The text input element for the new item has a separate closing tag:

<input id="newtodo" type="text" placeholder="New todo item" @keypress="addTodo" @blur="addTodo"></input>

But input elements have no permitted content2 3 and thus are empty elements.

In HTML, using a closing tag on an empty element is usually invalid. For example, <input type="text"></input> is invalid HTML.4

The jsFiddle syntax highlighting also points this out (I added some colored boxes to point this out):

jsFiddle syntax highlighting

Because of this, the closing slash can be added to the end of the start tag:

<input id="newtodo" type="text" placeholder="New todo item" @keypress="addTodo" @blur="addTodo" />

1https://eslint.org/docs/rules/no-unused-vars

2https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input

3https://html.spec.whatwg.org/multipage/input.html#the-input-element

4https://developer.mozilla.org/en-US/docs/Glossary/Empty_element

\$\endgroup\$
3
  • 2
    \$\begingroup\$ Thank you very much! This is a great answer :) \$\endgroup\$ Commented Feb 14, 2019 at 0:04
  • \$\begingroup\$ @SamOnela I'm seeing the mentioned vue todo vuejs.org/v2/guide/list.html but it appears there is not method defined that removes a todo item... \$\endgroup\$
    – Robert
    Commented Feb 21, 2021 at 16:07
  • 1
    \$\begingroup\$ @RobertRocha - that is true - notice that the remove button is coded as <button v-on:click="$emit(\'remove\')">Remove</button> so when it is clicked it doesn't call a method on the todo-item component but rather emits an event to the parent. Note that the parent element has v-on:remove="todos.splice(index, 1)" on the <li is="todo-item" element so it doesn't need a method but one could certainly call a method in either place if it was necessary. \$\endgroup\$ Commented Feb 22, 2021 at 18:50

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.