كل المقالات

Vue.js - الارتقاء من المكونات عديمة التصيير إلى الدوال التركيبية

شرح لمفهوم المكوّنات عديمة التصيير، ولطريقة تحويلها إلى دوال تركيبية بالواجهة التركيبية (Composition API) في Vue.js.

نُشر في 8 د للقراءة

Vue.js - الارتقاء من المكونات عديمة التصيير إلى الدوال التركيبية

مقدمة

يمكن عرض نتائج الإكمال التلقائي في قائمة أو شبكة بطاقات مع استعمال منطق البحث نفسه. سنبني هذا المنطق في مكوّن عديم التصيير، ثم ننقله إلى دالة تركيبية لنقارن طريقة ربط كل منهما بالقالب.

ما هو المكون عديم التصيير؟

المكوّن عديم التصيير مكوّن لا يصيّر عناصر HTML خاصة به. مهمّته أن يوفّر المنطق والبيانات لمن يستعمله: يستقبل الخاصيات، ويكشف البيانات والدوال عبر المنافذ، ويمكنه إطلاق أحداث. ويترك للمكوّن الأب أن يحدّد ما يُعرض عبر المنافذ ذات النطاق.

ما فائدة استخدام المكون عديم التصيير؟

فائدته الأولى إعادة الاستعمال: يُكتب المنطق مرة واحدة ويُستعمل في مواضع كثيرة من التطبيق. والثانية حرية العرض: يتكيّف المكوّن مع حالات ومتطلبات مختلفة، لأن الشكل ليس جزءًا منه. مثال ذلك حقل إدخال يتولّى التحقق من القيمة، ويكشف للمكوّن الأب خاصيات المنفذ اللازمة ليعرض هو رسائل التحقق بالشكل الذي يريد. ولمزيد من التفصيل في هذا المثال، راجع مقالة Renderless components pattern in Vue 3.

كيف تنشئ مكونًا عديم التصيير في Vue.js؟

لنبيّن ذلك بمثال: مكوّن إكمال تلقائي AutoComplete يبحث عن عنصر في قائمة. يستقبل المكوّن قائمة العناصر، ودالة بحث اختيارية، في خاصيتين. ويحتفظ هو باستعلام البحث، فيصفّي العناصر كلما تغيّر الاستعلام، ويمرّر النتائج إلى منفذ يصيّرها فيه المكوّن الأب كما يشاء. ويُطلق حدث select ليعرف المكوّن الأب أيّ عنصر اختير.

يكشف منفذ AutoComplete استعلام البحث والنتائج ودالتين: واحدة لتحديث الاستعلام، وأخرى لاختيار عنصر. يستعملها المكوّن الأب لربط حقل الإدخال وأزرار النتائج بمنطق البحث.

مثال بسيط

AutoComplete.tsts
import { defineComponent, ref } from 'vue'

const AutoComplete = defineComponent({
  props: {
    items: {
      type: Array,
      required: true,
    },
    searchFn: {
      type: Function,
      required: false,
    },
  },
  emits: ['select'],
  setup(props, { emit, slots }) {
    const searchQuery = ref('') // استعلام البحث الذي يكتبه المستخدم
    const results = ref([]) // نتائج البحث عن الاستعلام الحالي
    const onSearch = () => {
      if (searchQuery.value.length === 0) {
        results.value = []
        return
      }

      results.value = props.searchFn
        ? props.searchFn(searchQuery.value)
        : props.items.filter((item: any) => {
            return item.toLowerCase().includes(searchQuery.value.toLowerCase())
          })
    }

    const onSearchQueryChange = (val: string) => {
      searchQuery.value = val
      onSearch()
    }

    const select = (result: any) => {
      emit('select', result)
      searchQuery.value = ''
      results.value = []
    }

    return () => slots.default?.({
      searchQuery: searchQuery.value,
      results: results.value,
      onSearchQueryChange,
      select,
    })
  },
})

export default AutoComplete

نستعمل المكوّن هنا لتصيير حقل إدخال يُكتب فيه استعلام البحث، وتحته النتائج في قائمة غير مرتّبة.

vue
<script setup lang="ts">
import AutoComplete from './AutoComplete'

const items = ['Vue.js', 'React', 'Angular', 'Svelte', 'Ember', 'Meteor']
</script>

<template>
  <div class="p-2">
    <AutoComplete v-slot="{ searchQuery, results, onSearchQueryChange }" :items="items">
      <input
        class="bg-transparent p-2 border rounded"
        type="text"
        :value="searchQuery"
        @input="onSearchQueryChange(($event.target as HTMLInputElement).value)"
      />
      <ul>
        <li v-for="result in results" :key="result">
          {{ result }}
        </li>
      </ul>
    </AutoComplete>
  </div>
</template>

النتيجة

ابحث في: Vue.js, React, Angular, Svelte, Ember, Meteor

مثال متقدم

لنفترض أن لدينا قائمة أطر عمل مع شعاراتها، ونريد عرضها شبكةً من البطاقات. العناصر هنا كائنات لا نصوص، فنمرّر إلى AutoComplete دالة بحث خاصة تبحث في اسم الإطار.

vue
<script setup lang="ts">
import AutoComplete from './AutoComplete'

const items = [
  { name: 'Vue.js', logo: '/blog-img/logos/vue.svg' },
  { name: 'React', logo: '/blog-img/logos/react.svg' },
  { name: 'Angular', logo: '/blog-img/logos/angular.svg' },
  { name: 'Svelte', logo: '/blog-img/logos/svelte.svg' },
  { name: 'Ember', logo: '/blog-img/logos/ember.svg' },
  { name: 'Meteor', logo: '/blog-img/logos/meteor.svg' },
]

const filterLibs = (searchQuery: string) => {
  return items.filter(item => item.name.toLowerCase().includes(searchQuery.toLowerCase()))
}
</script>

<template>
  <div class="p-2">
    <AutoComplete v-slot="{ searchQuery, results, onSearchQueryChange }" :items="items" :search-fn="filterLibs">
      <input
        class="bg-transparent p-2 border rounded"
        type="text"
        :value="searchQuery"
        @input="onSearchQueryChange(($event.target as HTMLInputElement).value)"
      />
      <div class="grid grid-cols-3 gap-4 p-4">
        <div v-for="result in results" :key="result.name" class="rounded shadow p-4">
          <img :src="result.logo" :alt="result.name" class="w-24 h-24 mx-auto" />
          <p class="text-center">{{ result.name }}</p>
        </div>
      </div>
    </AutoComplete>
  </div>
</template>

النتيجة

ابحث في: Vue.js, React, Angular, Svelte, Ember, Meteor

اختيار عنصر

المثالان السابقان يبحثان فقط. والمكوّن يستطيع أيضًا أن يُعلم المكوّن الأب بالعنصر الذي اختاره المستخدم. فالمنفذ يستقبل دالة select، واستدعاؤها يُطلق حدث select ومعه العنصر المختار، ثم يمسح استعلام البحث ويفرّغ قائمة النتائج.

وبتفريغ القائمة يزول من الصفحة الزرّ الذي نُقر عليه، ولذلك يعيد المثال التركيز إلى حقل الإدخال، فلا يفقد من يتنقّل بلوحة المفاتيح موضعه.

vue
<script setup lang="ts">
import { ref } from 'vue'
import AutoComplete from './AutoComplete'

const items = ['Vue.js', 'React', 'Angular', 'Svelte', 'Ember', 'Meteor']
const selected = ref('')
const input = ref<HTMLInputElement>()

const onSelect = (item: string) => {
  selected.value = item
  // اختفى الزرّ الذي نُقر عليه، فيعود التركيز إلى حقل الإدخال
  input.value?.focus()
}
</script>

<template>
  <div class="p-2">
    <AutoComplete
      v-slot="{ searchQuery, results, onSearchQueryChange, select }"
      :items="items"
      @select="onSelect"
    >
      <input
        ref="input"
        class="bg-transparent p-2 border rounded"
        type="text"
        :value="searchQuery"
        @input="onSearchQueryChange(($event.target as HTMLInputElement).value)"
      />
      <ul>
        <li v-for="result in results" :key="result">
          <button type="button" @click="select(result)">{{ result }}</button>
        </li>
      </ul>
    </AutoComplete>
    <p>العنصر المختار: {{ selected || 'لا شيء بعد' }}</p>
  </div>
</template>

النتيجة

ابحث في: Vue.js, React, Angular, Svelte, Ember, Meteor

العنصر المختار: لا شيء بعد

يمكن وضع حالة البحث ومنطق التصفية في دالة تركيبية أيضًا، لأنهما لا يعتمدان على عناصر الواجهة. عندها يستقبل المكوّن الذي يستدعي الدالة القيم التفاعلية مباشرةً، بدلًا من الحصول عليها عبر منفذ.

ما هي الدالة التركيبية؟

الدالة التركيبية دالة تضمّ منطقًا له حالة باستعمال الواجهة التركيبية في Vue. ويمكنها استعمال واجهة التفاعلية وخطافات دورة الحياة وحقن الاعتماديات. وبها يُستعمل المنطق نفسه في عدة مكوّنات.

مثال بسيط

نبدأ بدالة تركيبية تتولّى منطق البحث عن عنصر في قائمة. تستقبل قائمة العناصر، ودالة بحث اختيارية، وتُرجع استعلام البحث والنتائج التي تتجدّد كلما تغيّر الاستعلام.

useAutocomplete.tsts
import { computed, ref } from 'vue'

export default function useAutocomplete<T = string>(items: T[], searchFn?: (query: string) => T[]) {
  const searchQuery = ref('') // استعلام البحث الذي يكتبه المستخدم
  const results = computed<T[]>(() => {
    // نتائج البحث عن الاستعلام الحالي
    if (searchQuery.value.length === 0) {
      return []
    }

    return searchFn
      ? searchFn(searchQuery.value)
      : items.filter((item) => {
          return String(item).toLowerCase().includes(searchQuery.value.toLowerCase())
        })
  })

  return {
    searchQuery,
    results,
  }
}

والآن نستعمل الدالة التركيبية لتصيير حقل الإدخال نفسه، وتحته النتائج في قائمة غير مرتّبة.

vue
<script setup lang="ts">
import useAutocomplete from './useAutocomplete'

const items = ['Vue.js', 'React', 'Angular', 'Svelte', 'Ember', 'Meteor']

const { searchQuery, results } = useAutocomplete(items)
</script>

<template>
  <div class="p-2">
    <input v-model="searchQuery" class="bg-transparent p-2 border rounded" type="text" />
    <ul>
      <li v-for="result in results" :key="result">
        {{ result }}
      </li>
    </ul>
  </div>
</template>

النتيجة

ابحث في: Vue.js, React, Angular, Svelte, Ember, Meteor

بالدالة التركيبية أنجزنا البحث في هذا المثال بشيفرة أقل. ولا حاجة هنا إلى حدث لاختيار عنصر: المكوّن الذي يستدعي الدالة يملك القالب، فيستطيع عند النقر أن يحفظ العنصر ويفرّغ searchQuery بنفسه.

مثال متقدم

نمرّر الآن دالة بحث إلى الدالة التركيبية، ونصيّر النتائج شبكةً من البطاقات. ودالة البحث هي التي تصفّي القائمة بحسب استعلام البحث.

vue
<script setup lang="ts">
import useAutocomplete from './useAutocomplete'

const items = [
  { name: 'Vue.js', logo: '/blog-img/logos/vue.svg' },
  { name: 'React', logo: '/blog-img/logos/react.svg' },
  { name: 'Angular', logo: '/blog-img/logos/angular.svg' },
  { name: 'Svelte', logo: '/blog-img/logos/svelte.svg' },
  { name: 'Ember', logo: '/blog-img/logos/ember.svg' },
  { name: 'Meteor', logo: '/blog-img/logos/meteor.svg' },
]

const filterLibs = (searchQuery: string) => {
  return items.filter(item => item.name.toLowerCase().includes(searchQuery.toLowerCase()))
}

const { searchQuery, results } = useAutocomplete(items, filterLibs)
</script>

<template>
  <div class="p-2">
    <input v-model="searchQuery" class="bg-transparent p-2 border rounded" type="text" />
    <div class="grid grid-cols-3 gap-4 p-4">
      <div v-for="result in results" :key="result.name" class="rounded shadow p-4">
        <img :src="result.logo" :alt="result.name" class="w-24 h-24 mx-auto" />
        <p class="text-center">{{ result.name }}</p>
      </div>
    </div>
  </div>
</template>

النتيجة

ابحث في: Vue.js, React, Angular, Svelte, Ember, Meteor

الخاتمة

الطريقتان تصفّيان العناصر نفسها وتتركان الواجهة للمكوّن الذي يستعملهما. يوفّر المكوّن عديم التصيير الحالة والدوال عبر منفذ، ويُطلق حدثًا عند اختيار عنصر. أما الدالة التركيبية فتُرجع حالة البحث مباشرةً، ويكون اختيار العنصر ومسح الاستعلام مسؤولية المكوّن الذي يستدعيها.

فهرس المصطلحات

إنجليزيةعربية
renderless componentمكون عديم التصيير
composable functionدالة تركيبية
render functionدالة تصيير
renderتصيير
propsخاصيات
slotsمنافذ
scoped slotsمنافذ ذات نطاق