Vue.js Örnek Task Uygulaması

Vue.js ile ilgili ilk araştırmalarımda karşıma çıkan kaynaklardan biri Fatih Acet'e aitti1. En temel kapsamlı konulara doğru ilerlediği bu videolar sayesinde Vue.js'e ilk adımlarımı attığımı rahatlıkla ve samimiyetle söyleyebilirim.

AA

Kendisine bir kez daha, bu yazı nezdinde ayırdığı vakit, verdiği emek ve paylaştığı bilgilerden dolayı teşekkür ederim. Bahsettiği işlemleri pekiştirmek amacıyla Amazon Shopping Cart benzeri bir örnek gerçekleştirdiği video dizisini ilk izlediğimde süreci kavramam biraz zor olmuştu ve kendime benzer ve/veya mümkünse daha kapsamlı bir örnek hazırlamayı hedef olarak koymuştum. Vue ile ilgili dökümanları ve diğer eğitimleri takip ettiğim süreçte bir task uygulaması hazırlamaya karar verdim. Hazırlayacağım bu task uygulaması mümkün olduğu kadar dinamik olmalıydı. Bahsettiğim Vue’nun sunduğu dinamik yapıdan ziyade task içeriğindeki durumların da dışarıdan müdahalelerle şekillendirilebilmesi olarak düşünülebilir. Netice itibariyle, tamamen planladığım yapıya kavuşamamış olsam da bilgim dahilinde şimdilik amacıma ulaştığımı belirtebilirim. Bu vesile ile, hem bahsi geçen task uygulamadan, hem de zorlandığım noktalardan ve not ettiğim diğer konulardan bahsetmek istiyorum.

Vue.js Task Uygulaması

Vue İle Task Uygulaması

Uygulamaya başlamadan önce, uygulamanın amacını ve bu amacı gerçekleştirebilmesi için ne tür özelliklere sahip olması gerektiğini belirlemek gerekiyor. Aksi durumda, süreç içerisindeki kararlar sürecin kendisini bir çıkmaza sokabiliyor. Pek çok fikrin uygulamaya dönüştürülmesi aşamasında sıklıkla karşılaşılan hatalardan biri olan bu yaklaşım MVP (Minimum Viable Product / Uygulanabilir Asgari Ürün) olarak ifade edeceğimiz yaklaşımla çözümlenebilir. Bu sebeple, örnek olması açısından task uygulamasını planlarken öncelikli olarak belirlediğim niteliklerden bahsetmek istiyorum.

  • Core dosya ile browser temelinde işlemleri gerçeleştirmek,
  • Plugin kullanmamak. Veri çekerken fetch yerine deneme amaçlı axios kullandım2, dolayısıyla bu maddeyi kısmen esnetmiş oldum. Ancak, ilgili satırı fetch ile de gerçekleştirmek mümkün.
  • Sıralama, önceliklendirme ve filtreleme işlemleri gerçekleştirmek,
  • Component‘leri object olarak oluşturmak ve yönetmek,
  • Verileri API ile çekmek,
  • Bulma CSS framework kullanmak.

Task uygulamasına ait kodları incelemek (ayrıca code review’da bulunmak) isterseniz GitLab > vue-examples > 2_intermediate > task üzerinden ilgili içeriğe ulaşabilirsiniz.

Dosya Gereksinimleri

Yukarıda bahsi geçen özellikler için haciri kaynaklara erişim duyacağım. Elbette Vue UMD (Universal Module Definition) / Full (compiler + runtime)3 bunlardan en önemlisi. Ardından, Axios javascript dosyası1 ve son olarak da Bulma CSS için gerekli olan ve CSS dosyası gelmekte. Buefy plugin’ini bu uygulamada özellikle kullanmadım4. Ancak, göz aşinalığı olması açısından yayınladığım Vue CLI ve Örnek REST API Uygulaması başlıklı yazıya göz atabilirsiniz.

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.4/css/bulma.min.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.13/vue.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.18.0/axios.js"></script>

Bu tanımlamaların ardından, CSS özelleştirmelerine geçebiliriz. v-cloak kullanımını bir alışkanlık haline getirdiğim için ilk satırlarda bu tanımlamayı ele aldım. Ardından, transition için gerekli olan sınıflar ve Bulma özelleştirmeleri gelmekte. Modal’i hariç tutacak olursak, tüm işlemi tek bir component ve bu component içerisinde generate eden child component’lerle sağlayacağım.

<tasks-comp v-for="(task, index) in tasks" :key="task.slug" :title="task.title" :slug="task.slug" :search="searchQuery"></tasks-comp>

task-comp component’i tasks içeriğindeki tanımları alıp birer child component olarak işlemekte. Dolayısıyla, eklenen her tanım bir task status olarak işleme alınmakta. Öncelikli olarak ele alacağım 3 status bulunuyor; Active Tasks, In Progress ve Completed Tasks. slug tanımı ise props olarak child component’ler arasındaki task ilişkilerinde kullanılacak tanımlamalar.

tasks: [{
        title: "Active Tasks",
        slug: "taskactive"
      },
      {
        title: "In Progress",
        slug: "taskinprogress"
      },
      {
        title: "Completed Tasks",
        slug: "taskcompleted"
      }
    ]

Global olarak oluşturduğumuz task-comp‘imize de bakalım:

Vue.component("tasks-comp", {
  created() {
    this.taskOrderBy("priority");
  },
  props: {
    search: {},
    title: {
      required: true,
      type: String
    },
    slug: {
      required: true,
      type: String
    }
  },
  data() {
    return {
      taskData: this.$root[this.slug],
      filteredTasks: [],
      searchedTask: null
    };
  },
  template: `
  <div class="container" v-if="filteredTasks.length > 0">
    <transition name="fade"><div class="box" v-if="this.searchedTask">Aranan Kelime: <span class="tag is-light is-medium">{{ this.searchedTask }}</span></div></transition>

    <h2 class="title is-6" :title="this.title">{{ this.title.toUpperCase() }} <span class="tag is-link" v-text="filteredTasks.length"></span></h2>
    <h4 class="title is-6"><a class="is-link" @click="taskOrderBy('id')">Due Date</a> - <a class="is-link" @click="taskOrderBy('priority')">Priority</a></h4>
    <div class="columns is-12 is-multiline">
      <task-comp v-for="(task, index) in searchBy(search)" :key="slug + '-' + index" :task="task" :slug="slug" :index="index"></task-comp>
    </div>
    <hr />
  </div>
  `,
  methods: {
    taskOrderBy(type) {
      const sortCondition =
        type === "id" ?
        (x, y) => x.id - y.id :
        (x, y) => x.priority - y.priority;
      this.filteredTasks = this.taskData
        .slice()
        .sort(sortCondition)
        .reverse();
    },
    searchBy(val) {
      this.searchedTask = val;
      if (!this.searchedTask) return this.filteredTasks;
      let fTask = this.searchedTask.toLowerCase()

      return this.filteredTasks.filter(t => {
        return t.title.toLowerCase().includes(fTask) || t.description.toLowerCase().includes(fTask)
      })

    }
  },
  watch: {
    taskData: function (val) {
      this.filteredTasks = val;
    }
  }
});

Görüldüğü üzere task’ler taskData ve filteredTasks ile işleme alınmakta. Elbette bu işlem created() ile taskOrderBy() metotu üzerinden sağlanmakta. Bu method verileri belirlenen sıralama (ordering) biçimlerine göre (eklenme sırasına (id) ve önem derecesine (priority) göre) düzenleme yapıp taskData içeriğini filteredTasks‘e aktarmakta. Parent component içerisinde yer alan task-comp ise search method’una, task ve slug içeriklerini almakta ve kendi içeriğinde edindiğim bu verileri istediğim şekilde ele alabilmemi mümkün hale getirmekte.

<task-comp v-for="(task, index) in searchBy(search)" :key="slug + '-' + index" :task="task" :slug="slug" :index="index"></task-comp>

Global task-comp component içeriği ise şu şekilde:

Vue.component("task-comp", {
  props: {
    task: {
      required: true,
      type: Object,
      default: () => ({})
    },
    index: {
      required: true,
      type: Number
    },
    slug: {
      required: true,
      type: String
    }
  },
  data() {
    return {};
  },
  template: `
  <transition name="fade">
  <div class="column is-3">
  <div class="card has-equal-height">
   <header class="card-header"><h1 class="card-header-title">{{ task.title }}</h1></header>
      <div class="card-content">
        <div class="content">
          <p><span :class="['tag', 'is-info', this.$root.priorities[task.priority-1] ]">Öncelik: {{ task.priority }}</span><span class="tag">Tarih: <time :datetime="task.date">{{ task.date }}</time></span></p>
          <p v-html="task.description"></p>
    </div>
      </div>
      <component is="task-footer" v-on:setcompleted="taskcompleted(index, slug)" v-on:setinprogress="taskinprogress(index, slug)" :key="'button-' + index" :index="index" :slug="slug"></component>
  </div>
  </div>
  </transition>
  `,
  methods: {
    taskcompleted(val, tasktype) {
      if (tasktype == "taskactive") {
        const task = this.$root["taskactive"].splice(val, 1);
        this.$root["taskcompleted"].push(task[0]);
      } else if (tasktype == "taskcompleted") {
        const task = this.$root["taskcompleted"].splice(val, 1);
        this.$root["taskinprogress"].push(task[0]);
      } else if (tasktype == "taskinprogress") {
        const task = this.$root["taskinprogress"].splice(val, 1);
        this.$root["taskcompleted"].push(task[0]);
      } else {
        console.log('Error!')
      }
    },
    taskinprogress(val, tasktype) {
      if (tasktype == "taskactive") {
        const task = this.$root["taskactive"].splice(val, 1);
        this.$root["taskinprogress"].push(task[0]);
      } else if (tasktype == "taskcompleted") {
        const task = this.$root["taskcompleted"].splice(val, 1);
        this.$root["taskinprogress"].push(task[0]);
      } else if (tasktype == "taskinprogress") {
        const task = this.$root["taskinprogress"].splice(val, 1);
        this.$root["taskcompleted"].push(task[0]);
      } else {
        console.log('Error!')
      }
    }
  }
});

Bu component esasında en çok zorlandığım alan oldu. Özellikle oluşturulan component’ler arasında task’lerin aktarılması için kullanılacak butonlar ($emit işlemi) ve status alanları arasındaki ilişkiler için manuel olarak slug tanımlarını yapmak zorunda kaldım. İlerleyen zaman içerisinde özellikle bu component içeriğini yeniden ele almak gibi bir planım var. Diğer yandan dragula5 gibi bir drag & drop işlemini de çalışmaya dahil edebilirim.

Son olarak, task-footer component’i ile butonları ilgili task card’larına ekledim. Bu component ayrıca sahip olduğu status’e göre butonların davranışlarını da tanımlamakta.

Vue.component("task-footer", {
  props: {
    slug: {
      required: true,
      type: String
    }
  },
  data() {
    return {};
  },
  template: `
  <footer class="card-footer">
    <div class="buttons has-addons">
      <a v-if="slug !== 'taskcompleted'" :class="['card-footer-item', 'button', 'is-link']" v-on:click="$emit('setcompleted')">Completed</a>
      <a v-if="slug !== 'taskinprogress'" :class="['card-footer-item', 'button', 'is-warning']" v-on:click="$emit('setinprogress')">In Progress</a>
  </div>
 </footer>
  `
});

Tüm bu işlemler, yazının başında da belirttiğim gibi API üzerinden edinilen bilgiler üzerinden gerçekleştirilmekte. Bu aşamada beeceptor'dan faydalandım6. Günlük belirli limitler dahilinde erişim sunmasına karşın, ihtiyacımı oldukça basit bir şekilde karşılamakta. Sonrasında bu veriler işlenmekte ve ilgili component’lere aktarılmakta. Sihirin gerçekleştirildiği alan (Vue instance) şöyle:

var app = new Vue({
  el: "#app",
  data: {
    tasks: [{
        title: "Active Tasks",
        slug: "taskactive"
      },
      {
        title: "In Progress",
        slug: "taskinprogress"
      },
      {
        title: "Completed Tasks",
        slug: "taskcompleted"
      }
    ],
    modelActive: false,
    priorities: [
      "is-primary",
      "is-info",
      "is-success",
      "is-warning",
      "is-danger"
    ],
    newtask: {
      id: null,
      title: null,
      priority: 1,
      description: null,
      date: null
    },
    "taskactive": [],
    "taskinprogress": [],
    "taskcompleted": [],
    searchQuery: null
  },
  methods: {
    submitnewtask() {
      // this.$root["taskactive"].push(this.newtask);
      let data = this.newtask
      axios
        .put("https://vuetest.free.beeceptor.com/add", {
          data: {
            tasks: [data]
          }
        })
        .then(r => console.log(r.status))
        .catch(e => console.log(e));
      this.$root.taskactive = [];
      this.getTaskFromAPI();
      this.newtask = [];
    },
    getTaskFromAPI() {
      axios({
        method: "get",
        url: "https://vuetest.free.beeceptor.com",
      }).then((resp => {
        this.$root.taskactive.push(...resp.data.data.tasks);
      })).catch(error => console.log(error))
    }
  },
  computed: {
    getDate() {
      let getCurrentDate = new Date().toISOString().split("T")[0];
      return getCurrentDate;
    },
    getNewID() {
      let getLastestID = this.$root["taskactive"].length;
      return getLastestID;
    }
  },
  created() {
    this.newtask.date = this.getDate;
    this.newtask.id = this.getNewID;
    this.getTaskFromAPI();
  },
  watch: {}
});

Evet, süreci mümkün olduğu kadar en yalın haliyle aktarmaya çalıştım. Bir sonraki aşamada uygulamayı single-file component olarak ele alacak ve süreci kolaylaştıracak -ya da karmaşayı ortadan kaldıracak- pluginlerle (nuxt, buefy, vue-dragula gibi) ilerleyeceğim. Son olarak, süreç içerisinde karşılaştığım problemlerde çözüm sunan Vue.js Türkiye7 grubuna da teşekkürler.