/ Gists / Vue.js

Gists - Vue.js

On gists

Function in props

Vue.js

example.vue #

// DataGrid.vue
<script setup>
const props = defineProps({
  items: {
    type: Array,
    required: true
  },
  // Render funkce pro transformaci dat nebo komplexní logiku
  cellRenderer: {
    type: Function,
    default: null
  },
  // Funkce pro custom formátování dat v buňce
  formatters: {
    type: Object,
    default: () => ({})
  }
})

const formatCell = (value, column) => {
  // Pokud existuje custom formatter pro tento sloupec, použij ho
  if (props.formatters[column]) {
    return props.formatters[column](value)
  }
  
  // Základní formátování podle typu dat
  if (value instanceof Date) {
    return value.toLocaleDateString()
  }
  if (typeof value === 'number') {
    return value.toLocaleString()
  }
  return value
}
</script>

<template>
  <div class="data-grid">
    <slot name="header" />
    
    <div class="grid-body">
      <template v-for="item in items" :key="item.id">
        <!-- Prioritně použij slot pokud existuje -->
        <slot 
          name="row" 
          :item="item"
          :format="formatCell"
        >
          <!-- Fallback na render funkci -->
          <template v-if="cellRenderer">
            {{ cellRenderer(item, formatCell) }}
          </template>
          <!-- Základní fallback -->
          <template v-else>
            <div class="grid-row">
              <div 
                v-for="(value, key) in item" 
                :key="key"
                class="grid-cell"
              >
                {{ formatCell(value, key) }}
              </div>
            </div>
          </template>
        </slot>
      </template>
    </div>

    <slot name="footer" />
  </div>
</template>

// Použití:
<script setup>
const data = [
  { 
    id: 1,
    name: 'Produkt 1',
    price: 1299.99,
    lastUpdated: new Date('2024-03-15'),
    status: 'active'
  },
  // ...
]

// Custom formátování pro specifické sloupce
const formatters = {
  price: (value) => `${value.toLocaleString()} Kč`,
  status: (value) => ({
    'active': '✅ Aktivní',
    'inactive': '❌ Neaktivní'
  }[value] || value)
}

// Komplexní transformace dat
const renderComplexRow = (item, format) => {
  const statusClass = item.status === 'active' ? 'text-green' : 'text-red'
  
  return `
    <div class="grid-row">
      <div class="font-bold">${item.name}</div>
      <div>${format(item.price, 'price')}</div>
      <div class="${statusClass}">${format(item.status, 'status')}</div>
      <div>${format(item.lastUpdated)}</div>
    </div>
  `
}
</script>

<template>
  <!-- Základní použití se slotem -->
  <DataGrid :items="data" :formatters="formatters">
    <template #row="{ item, format }">
      <div class="grid-row">
        <div>{{ item.name }}</div>
        <div>{{ format(item.price, 'price') }}</div>
        <div>{{ format(item.status, 'status') }}</div>
      </div>
    </template>
  </DataGrid>

  <!-- Použití s render funkcí pro specifické případy -->
  <DataGrid 
    :items="data"
    :formatters="formatters"
    :cell-renderer="renderComplexRow"
  />
</template>

On gists

Provider->Consumer

Patterns.dev Plugin patterns Vue.js

Provider.vue #

<!-- https://learn-vuejs.github.io/vue-patterns/patterns/#dependency-injection -->

<template>
  <div>
    <slot v-bind="{ state, actions }" />
  </div>
</template>

<script>
export default {
  computed: {
    state() {
      return {
        label: 'button',
      };
    },
    actions() {
      return {
        click: this.click,
      };
    },
  },
  methods: {
    click() {
      console.log('Clicked');
    },
  },
}
</script>

On gists

Vue patterns comparison

Plugin patterns Vue.js

claudeai.txt #

:D Jo, přesně tak! To jste trefil naprosto přesně.

V praxi to často vypadá takhle:

"Super-Mega-Awesome-Provider-Container-Renderless-Composition-Pattern™️"
```vue
<template>
  <slot v-bind="data">
    <default-view v-bind="data" />
  </slot>
</template>
```

A ve finále je to prostě:
- Komponenta
- Se slotem
- A výchozím vzhledem
- Která předává data dál

Často se z jednoduchých věcí dělá zbytečná věda jen proto, aby se dalo napsat článek nebo přednášet na konferenci. Místo:
"Hey, dej si tam slot a budeš to moct přepsat" 
máme:
"Implementace Advanced Renderless Component Pattern s Dependency Injection a Composable Factory Integration" 😅

Proto je super váš přístup - dívat se na věci prakticky a nenechat se unést přehnaně komplikovanými názvy pro jednoduché koncepty.

On gists

Renderless with default layouts

Patterns.dev Plugin patterns Vue.js

DataProvider.vue #

<!-- DataProvider.vue -->
<template>
  <slot 
    :data="data"
    :loading="loading"
    :error="error"
  >
    <component 
      :is="layouts[variant]" 
      :data="data"
      :loading="loading"
      :error="error"
    />
  </slot>
</template>

<script setup>
import DefaultLayout from './layouts/DefaultLayout.vue'
import CompactLayout from './layouts/CompactLayout.vue'
import CardLayout from './layouts/CardLayout.vue'

const props = defineProps({
  variant: {
    type: String,
    default: 'default',
    validator: (value) => ['default', 'compact', 'card'].includes(value)
  }
})

const layouts = {
  default: DefaultLayout,
  compact: CompactLayout,
  card: CardLayout
}
</script>

On gists

Container Pattern

Patterns.dev Plugin patterns Vue.js

DogImagesContainer.vue #

<!-- DogImagesContainer.vue -->

<template>
  <DogImages :dogs="dogs" />
</template>

<script setup>
  import { ref, onMounted } from "vue";
  import DogImages from "./DogImages.vue";

  const dogs = ref([]);

  onMounted(async () => {
    const response = await fetch(
      "https://dog.ceo/api/breed/labrador/images/random/6"
    );
    const { message } = await response.json();
    dogs.value = message;
  });
</script>

On gists

Data Provider

Patterns.dev Plugin patterns Vue.js

dataprovider01.vue #

<template>
  <slot :checkbox="checkbox" :toggleCheckbox="toggleCheckbox"></slot>
</template>

<script setup>
  import { ref, reactive } from "vue";

  const API_ENDPOINT_URL = "https://official-joke-api.appspot.com/random_joke";

  const data = reactive({
    setup: null,
    punchline: null,
  });
  const loading = ref(false);

  const fetchJoke = async () => {
    loading.value = true;

    const response = await fetch(API_ENDPOINT_URL);
    const responseData = await response.json();

    data.setup = responseData.setup;
    data.punchline = responseData.punchline;
    loading.value = false;
  };

  fetchJoke();
</script>



// usage
<template>
  <DataProvider v-slot="{ data, loading }">
    <div class="joke-section">
      <p v-if="loading">Joke is loading...</p>
      <p v-if="!loading">{{ data.setup }}</p>
      <p v-if="!loading">{{ data.punchline }}</p>
    </div>
  </DataProvider>

  <DataProvider v-slot="{ data, loading }">
    <p v-if="loading">Hold on one sec...</p>
    <div v-else class="joke-section">
      <details>
        <summary>{{ data.setup }}</summary>
        <p>{{ data.punchline }}</p>
      </details>
    </div>
  </DataProvider>
</template>

<script setup>
  import DataProvider from "./components/DataProvider.vue";
</script>

On gists

Renderless

Patterns.dev Plugin patterns Vue.js

MouseTracker01.vue #

<!-- MouseTracker.vue -->
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
  
const x = ref(0)
const y = ref(0)
 
const update = e => {
  x.value = e.pageX
  y.value = e.pageY
}
 
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
</script>
 
<template>
  <slot :x="x" :y="y"/>
</template>

On gists

Awaited computed from composable - special pattern by me

Vue.js

useAnyComposable.js #

// 1 - composable
import { ref, computed, watchEffect } from 'vue';
import useSomeOtherComposable from './useSomeOtherComposable';

export default function useMyComposable() {
  const { asyncValue1, asyncValue2 } = useSomeOtherComposable();

  const resolved = ref(false);

  // Hodnoty, které vrátíme, až budou splněny podmínky
  const computedValue1 = ref(null);
  const computedValue2 = ref(null);

  // Proměnná promise, která se splní, když budou data připravena
  const dataReady = new Promise((resolve) => {
    watchEffect(() => {
      if (asyncValue1.value === 'desiredValue1' && asyncValue2.value === 'desiredValue2') {
        computedValue1.value = asyncValue1.value;
        computedValue2.value = asyncValue2.value;
        resolved.value = true;
        resolve();
      }
    });
  });


 /*
  // i watchEffect lze zastavit
  
      const dataReady = new Promise((resolve) => {
      const stopEffect = watchEffect(() => {
        if (asyncValue1.value === 'desiredValue1' && asyncValue2.value === 'desiredValue2') {
          computedValue1.value = asyncValue1.value;
          computedValue2.value = asyncValue2.value;
          resolve(); // Vyřeší Promise, když jsou hodnoty požadované
          stopEffect(); // Zastaví watchEffect, protože už není potřeba
        }
      });
    });
 
 */



  // Vrátíme computed hodnoty i promise
  return {
    computedValue1,
    computedValue2,
    dataReady,
    resolved
  };
}

// usage - component
import { defineComponent, onMounted } from 'vue';
import useMyComposable from './useMyComposable'; // another composable which returns computed

export default defineComponent({
  setup() {
    const { computedValue1, computedValue2, dataReady } = useMyComposable();

    onMounted(async () => {
      await dataReady; // Počkáme, až budou hodnoty připravené
      console.log('Values are ready:', computedValue1.value, computedValue2.value);
    });

    return {
      computedValue1,
      computedValue2
    };
  }
});


// 2 - watch insteadof watchEffect
import { ref, watch } from 'vue';
import useSomeOtherComposable from './useSomeOtherComposable';

export default function useMyComposable() {
  const { asyncValue1, asyncValue2 } = useSomeOtherComposable();

  const computedValue1 = ref(null);
  const computedValue2 = ref(null);

  const dataReady = new Promise((resolve) => {
    const stopWatching = watch(
      [asyncValue1, asyncValue2],
      ([newVal1, newVal2]) => {
        if (newVal1 === 'desiredValue1' && newVal2 === 'desiredValue2') {
          computedValue1.value = newVal1;
          computedValue2.value = newVal2;
          resolve(); // Vyřeší Promise, jakmile hodnoty odpovídají požadavkům
          stopWatching(); // Zastaví sledování, protože už není potřeba
        }
      },
      { immediate: true } // Sleduj hned od začátku
    );
  });

  return {
    computedValue1,
    computedValue2,
    dataReady
  };
}


// component usage
import { defineComponent, onMounted } from 'vue';
import useMyComposable from './useMyComposable';

export default defineComponent({
  setup() {
    const { computedValue1, computedValue2, dataReady } = useMyComposable();

    onMounted(async () => {
      await dataReady; // Počkáme, až budou hodnoty připravené
      console.log('Values are ready:', computedValue1.value, computedValue2.value);
    });

    return {
      computedValue1,
      computedValue2
    };
  }
});


// ---------------------------------------------------
// geolocation checker aka MSP 
// ---------------------------------------------------
import { ref, computed } from 'vue';
import { useMyComposable } from './path/to/composable';

export default {
  setup() {
    // Definuj proměnné na úrovni komponenty
    const computedValue1 = ref(null);
    const computedValue2 = ref(null);
    const dataReady = ref(false);

    const check = () => {
      const { computedValue1: cv1, computedValue2: cv2, dataReady: dr } = useMyComposable();
      
      // Přiřaď hodnoty z composable do definovaných proměnných
      computedValue1.value = cv1.value;
      computedValue2.value = cv2.value;
      dataReady.value = dr.value;
    };

    // Spusť funkci nebo ji použij, když potřebuješ načíst data
    check();

    // Můžeš nyní přistupovat k computedValue1 a computedValue2 přímo mimo `check`
    console.log(computedValue1.value, computedValue2.value);

    return {
      computedValue1,
      computedValue2,
      dataReady,
      check,
    };
  },
};

On gists

Renderless component approach by Adam Wathan

Vue.js

LinkList.vue #


<!--
Url: https://adamwathan.me/renderless-components-in-vuejs/

Jeho pristup je predavani atributu a eventu jako cely objekt, moje je  :bookmarkNow a prijde mi to lepsi teda ... ale ukladam.

-->

<script setup>
const props = defineProps({
  links: {
    type: Array,
    required: true
  }
})

const bookmark = (link) => {
  link.bookmarked = !link.bookmarked;
}
</script>

<template>
  <slot 
    v-for="(link, index) in links" 
    :key="link.id"
    :link="link"
    :bookmarkNow="() => bookmark(link)"
    :bookmarkButtonAttrs="{
      style: link.bookmarked ? 'font-weight: bold;' : ''
    }"
    :bookmarkButtonEvents="{
      click: () => bookmark(link)
    }"
  />
</template>

On gists

Vue.js: ShallowRef by MichaelThiessen

Vue.js

ShallowRef.vue #

<script setup>
import { shallowRef, watchEffect, triggerRef } from 'vue'

  const user = shallowRef({
    name: 'John',
    job: 'BFU',
    age: 90
  })

// Log the user whenever it changes
  watchEffect(() => {
    console.log('LOG: watchEffect', user)
  });
 
 // Update nested state (no log happens)
 setTimeout(() => {
   user.value.name = 'Martin';
   console.log('LOG: user name changed')
 }, 2000)
 
 
 // Force a reactive update to trigger
  setTimeout(() => {
    triggerRef(user);
    console.log('LOG: whole object changed')
  }, 4000)

  // [user object]
</script>

<