Search code examples
javascriptvue.jsmodal-dialogvue-componentuser-input

How to create a vue modal that sends user input to another component


I am trying to create a modal component that takes in user input, and upon saving that information, is displayed within another component. For example, a user is prompted to input their first and last name respectively in a modal component (Modal.vue). Once the user saves that data (a submit method on the modal), the data is displayed on another component (InputItem.vue).

Currently, I have a CreateEvent.vue component that houses the input elements, a modal.vue component that is the modal, an EventItem.vue component, that will display what is entered on once CreateEvent is executed, an EventsList.vue component that displays all the events that a user creates and finally, app.vue which houses the Modal and Events components.

I have been able to successfully get this CRUD functionality working without the modal component, but once I add the modal, I am getting confused.

If you could help lead me in the right direction, I would appreciate that!

Modal.vue

<template>
  <transition name="modal-fade">
    <div class="modal-backdrop">
      <div
        class="modal"
        role="dialog"
        aria-labelledby="modalTitle"
        aria-describedby="modalDescription"
      >
        <header class="modal-header" id="modalTitle">
          <slot name="header">
            Create an Event
            <button
              type="button"
              class="btn-close"
              @click="close"
              aria-label="Close modal"
            >
              x
            </button>
          </slot>
        </header>
        <section class="modal-body" id="modalDescription">
          <slot name="body">
            <div @keyup.enter="addTodo">
              <input
                type="text"
                class="todo-input"
                placeholder="What needs to be done"
                v-model="newTodo"
              />
              <input
                type="text"
                placeholder="add an emoji?"
                v-model="newEmoji"
              />
            </div>
            <button @click="doneEdit">Create Event</button>
            <button @click="cancelEdit">Cancel</button>
          </slot>
        </section>
        <footer class="modal-footer">
          <slot name="footer">
            I'm the default footer!

            <button
              type="button"
              class="btn-green"
              @click="close"
              aria-label="Close modal"
            >
              Close me!
            </button>
          </slot>
        </footer>
      </div>
    </div>
  </transition>
</template>

<script>
export default {
  name: 'modal',
  data() {
    return {
      newTodo: '',
      newEmoji: '',
      idForTodo: this.todos.length + 1
    }
  },
  methods: {
    close() {
      this.$emit('close')
    },
    addTodo() {
      if (this.newTodo.trim().length == 0) return

      this.todos.push({
        id: this.idForTodo,
        title: this.newTodo,
        emoji: this.newEmoji
      })
      this.newTodo = ''
      this.newEmoji = ''
      this.idForTodo++
    }
  }
}
</script>

CreateEvent.vue

<template>
  <div @keyup.enter="addTodo">
    <input
      type="text"
      class="todo-input"
      placeholder="What needs to be done"
      v-model="newTodo"
    />
    <input type="text" placeholder="add an emoji?" v-model="newEmoji" />
  </div>
</template>

<script>
export default {
  props: {
    todos: {
      type: Array
    }
  },
  data() {
    return {
      newTodo: '',
      newEmoji: '',
      idForTodo: this.todos.length + 1
    }
  },
  methods: {
    addTodo() {
      if (this.newTodo.trim().length == 0) return

      this.todos.push({
        id: this.idForTodo,
        title: this.newTodo,
        emoji: this.newEmoji
      })
      this.newTodo = ''
      this.newEmoji = ''
      this.idForTodo++
    }
  }
}
</script>

EventItem.vue

<template>
  <div class="todo-item">
    <h3 class="todo-item--left">
      <!-- <span v-if="!editing" @click="editTodo" class="todo-item--label">
        {{ title }}
        {{ emoji }}
      </span> -->
      <input
        class="todo-item--edit"
        type="text"
        v-model="title"
        @click="editTitle"
        @blur="doneEdit"
      />
      <input
        class="todo-item--edit"
        type="text"
        v-model="emoji"
        @click="editEmoji"
        @blur="doneEdit"
      />

      <!-- <button @click="doneEdit">Update</button>
      <button @click="cancelEdit">Cancel</button> -->
    </h3>
    <button class="remove-item" @click="removeTodo(todo.id)">✘</button>
  </div>
</template>

<script>
export default {
  name: 'todo-item',
  props: {
    todo: {
      type: Object,
      required: true
    }
  },
  data() {
    return {
      id: this.todo.id,
      title: this.todo.title,
      emoji: this.todo.emoji,
      editing: this.todo.editing,
      beforeEditCacheTitle: this.todo.title,
      beforeEditCacheEmoji: this.todo.emoji
    }
  },
  methods: {
    editTitle() {
      this.beforeEditCacheTitle = this.title
      this.editing = true
    },
    editEmoji() {
      this.beforeEditCacheEmoji = this.emoji
      this.editing = true
    },
    doneEdit() {
      if (this.title.trim() == '') {
        this.title = this.beforeEditCacheTitle
      }
      if (this.emoji.trim() == '') {
        this.emoji = this.beforeEditCacheEmoji
      }

      this.editing = false
      this.$emit('finishedEdit', {
        id: this.id,
        title: this.title,
        emoji: this.emoji,
        editing: this.editing
      })
    },
    cancelEdit() {
      this.title = this.beforeEditCacheTitle
      this.emoji = this.beforeEditCacheEmoji
      this.editing = false
    },
    removeTodo(id) {
      this.$emit('removedTodo', id)
    }
  }
}
</script>

Events.vue

<template>
  <div>
    <transition-group
      name="fade"
      enter-active-class="animated fadeInUp"
      leave-active-class="animated fadeOutDown"
    >
      <EventItem
        v-for="todo in todosFiltered"
        :key="todo.id"
        :todo="todo"
        @removedTodo="removeTodo"
        @finishedEdit="finishedEdit"
      />
    </transition-group>
  </div>
</template>

<script>
import EventItem from '@/components/EventItem'

export default {
  components: {
    EventItem
  },
  data() {
    return {
      filter: 'all',
      todos: [
        {
          id: 1,
          title: 'Eat sushi',
          emoji: '💵',
          editing: false
        },
        {
          id: 2,
          title: 'Take over world',
          emoji: '👨🏽‍💻',
          editing: false
        }
      ]
    }
  },
  computed: {
    todosFiltered() {
      if (this.filter == 'all') {
        return this.todos
      }
    }
  },
  methods: {
    removeTodo(id) {
      const index = this.todos.findIndex(item => item.id == id)
      this.todos.splice(index, 1)
    },
    finishedEdit(data) {
      const index = this.todos.findIndex(item => item.id == data.id)
      this.todos.splice(index, 1, data)
    }
  }
}
</script>

app.vue

<template>
  <div id="app" class="container">
    <button type="button" class="btn" @click="showModal">
      Create Event
    </button>

    <Modal v-show="isModalVisible" @close="closeModal" />
    <Events />
  </div>
</template>

<script>
import Events from './components/Events'
import Modal from './components/Modal'

export default {
  name: 'App',
  components: {
    Events,
    Modal
  },
  data() {
    return {
      isModalVisible: false
    }
  },
  methods: {
    showModal() {
      this.isModalVisible = true
    },
    closeModal() {
      this.isModalVisible = false
    }
  }
}
</script>

Solution

  • The modal component should emit the values instead of pushing it into the todos array. When it emits it, the parent component (App.vue) listens for the emitted items.

    I would do something like this

    Modal.vue

    <template>
       ...
    // header
    <section class="modal-body" id="modalDescription">
      <slot name="body">
        <div @keyup.enter="addTodo">
          ...
        </div>
        <button @click="handleModalSubmit">Create Event</button>
      ...
      //footer
      ...
    </template>
    
    <script>
    export default {
      ...
      data() {
        ...
      },
      methods: {
        ...,
        handleModalSubmit() {
          this.$emit('todos-have-been-submitted', this.todos);
        },
        addTodo() {
          ...
          this.todos.push({
            id: this.idForTodo,
            title: this.newTodo,
            emoji: this.newEmoji
          })
          ...
        }
      }
    }
    </script>

    App.vue

    <template>
      ...
      <Modal
        @todos-have-been-submitted="handleTodoSubmission" //watch the 'todos-have-been-submitted" emission and trigger handleTodoSubmission method when the emission is detected
      />
      <Events 
        :todos="todos" // pass todos as a prop to the Events component
      />
      ...
    </template>
    
    <script>
    import Events from './components/Events'
    import Modal from './components/Modal'
    
    export default {
      name: 'App',
      components: {
        Events,
        Modal
      },
      data() {
        return {
        ...,
          todos: []
        }
      },
      methods: {
        ...,
        handleTodoSubmission(todos) {
          this.todos = [...todos];
        }
      }
    }
    </script>