/ Gists

Gists

On gists

10 communication parent -> child, siblings

cdruc vue

ParentChild.vue #

<script setup>
import ShoppingCart from "./ShoppingCart.vue";
import {ref} from "vue";
 
const isOpened = ref(false);
</script>
 
<template>
  <button @click="isOpened = true">Open cart</button>
  <ShoppingCart :is-opened="isOpened" @toggle="(value) => isOpened = value"/>
</template>

On gists

08 Videoplayer (skeleton) Composition API

cdruc vue

VideoPlayer.vue #

<script setup>
import axios from "axios";
import Player from "@vimeo/player";
import {throttle} from "lodash";
import ForwardIcon from "./svgs/ForwardIcon";
import BackwardIcon from "./svgs/BackwardIcon";
import LoadingIcon from "./svgs/LoadingIcon";
import {computed, onMounted, onUnmounted, ref, useSlots} from "vue";

const props = defineProps({
  lessonId: Number,
  trackProgress: Boolean
});

const speedIndex = ref(1);
let player = null;
const speedOptions = [0.75, 1, 1.25, 1.5, 1.75, 2.0];
const currentSpeed = computed(() => speedOptions[speedIndex.value]);

if (window.localStorage.getItem('speedIndex')) {
  speedIndex.value = parseInt(window.localStorage.getItem('speedIndex'));
}

onMounted(() => {
  player = new Player(useSlots().default()[0].el);
  player.on('play', () => player.setPlaybackRate(currentSpeed.value));

  if (props.trackProgress) {
    player.on('progress', throttle((event) => updateProgress(event.percent), 10000));
    player.on('ended', () => updateProgress(1));
  }
});

onUnmounted(() => {
  player.off('play');
  player.off('progress');
  player.off('ended');
});

function updateProgress(percent) {
  return axios.put(`/lessons/${props.lessonId}/progress`, {percent: percent});
}

function toggleSpeed() {
  if (typeof speedOptions[speedIndex.value + 1] !== "undefined") {
    speedIndex.value += 1;
  } else {
    speedIndex.value = 0;
  }

  window.localStorage.setItem('speedIndex', speedIndex.value);
  player.setPlaybackRate(currentSpeed.value);
}

async function goForward(seconds) {
  const currentTime = await player.getCurrentTime();
  const totalDuration = await player.getDuration();
  player.setCurrentTime(Math.min(totalDuration, (currentTime + seconds)));
}

async function goBack(seconds) {
  const currentTime = await player.getCurrentTime();
  player.setCurrentTime(Math.max(0, (currentTime - seconds)));
}
</script>

<template>
  <div class="relative aspect-w-16 aspect-h-9">
    <div class="flex absolute inset-0 justify-center items-center">
      <LoadingIcon class="w-14 h-14 text-white animate-spin"></LoadingIcon>
    </div>

    <slot></slot>
  </div>

  <div class="flex justify-end">
    <div class="grid grid-cols-5 w-full max-w-xl divide-x divide-gray-700">
      <button
        class="inline-flex justify-center items-center p-2 space-x-1 text-base font-medium text-white cursor-pointer hover:text-orange-600 focus:outline-none"
        @click="toggleSpeed()">
        <span class="hidden sm:inline-block">Speed:</span><span>{{ currentSpeed }}x</span>
      </button>

      <button
        class="inline-flex justify-center items-center p-2 space-x-1 text-base font-medium text-white cursor-pointer hover:text-orange-600 focus:outline-none"
        @click="goBack(5)">
        <BackwardIcon class="w-4 h-4"/>
        <span>5s</span>
      </button>

      <button
        class="inline-flex justify-center items-center p-2 space-x-1 text-base font-medium text-white cursor-pointer hover:text-orange-600 focus:outline-none"
        @click="goForward(5)">
        <span>5s</span>
        <ForwardIcon class="w-4 h-4"/>
      </button>

      <button
        class="inline-flex justify-center items-center p-2 space-x-1 text-base font-medium text-white cursor-pointer hover:text-orange-600 focus:outline-none"
        @click="goBack(10)">
        <BackwardIcon class="w-4 h-4"/>
        <span>10s</span>
      </button>

      <button
        class="inline-flex justify-center items-center p-2 space-x-1 text-base font-medium text-white cursor-pointer hover:text-orange-600 focus:outline-none"
        @click="goForward(10)">
        <span>10s</span>
        <ForwardIcon class="w-4 h-4"/>
      </button>
    </div>
  </div>
</template>

On gists

06 Loading script inside the Vue.js

cdruc vue

useScript.js #

import {onUnmounted} from "vue";

export default function (src) {
  return new Promise((resolve, reject) => {
    let script = document.querySelector(`script[src="${src}"]`);

    if (!script) {
      script = document.createElement("script");
      script.src = src;
      script.async = true;
      script.setAttribute("data-status", "loading");
      document.head.append(script);
    }

    if (script.getAttribute("data-status") === "loaded") {
      resolve();
    }

    function onScriptLoad() {
      resolve();
      script.setAttribute("data-status", "loaded");
    }

    function onScriptError() {
      reject();
      script.setAttribute("data-status", "error");
    }

    script.addEventListener("load", onScriptLoad);
    script.addEventListener("error", onScriptError);

    onUnmounted(() => {
      if (document.head.contains(script)) {
        script.removeEventListener("load", onScriptLoad);
        script.removeEventListener("error", onScriptError);
        document.head.removeChild(script);
      }
    })
  });
}

On gists

04 multiple v-model

cdruc vue

CheckoutForm.vue #

<template>
    <form class="my-20 mx-auto max-w-2xl">
        <h3 class="mb-2 mt-6 text-lg font-medium">Personal info</h3>
        <div class="grid grid-cols-2 gap-6 mb-6">
            <label class="block text-sm font-medium text-gray-700">
                First name
                <input 
                       class="block mt-1 w-full text-sm placeholder-gray-400 rounded-md border-gray-300 focus:ring-blue-500 focus:border-blue-500" 
                       type="text"
                       v-model="form.firstName"
                       placeholder="First name"
                />
            </label>

            <label class="block text-sm font-medium text-gray-700">
                Last name
                <input 
                       class="block mt-1 w-full text-sm placeholder-gray-400 rounded-md border-gray-300 focus:ring-blue-500 focus:border-blue-500" 
                       type="text"
                       v-model="form.lastName"
                       placeholder="Last name"
                />
            </label>

            <label class="block text-sm font-medium text-gray-700">
                E-mail
                <input 
                       class="block mt-1 w-full text-sm placeholder-gray-400 rounded-md border-gray-300 focus:ring-blue-500 focus:border-blue-500" 
                       type="email"
                       v-model="form.email"
                       placeholder="E-mail address"
                />
            </label>
        </div>
        
        <AddressFieldGroup
            label="Delivery address"
            v-model:street="form.deliveryAddress.street"
            v-model:streetNumber="form.deliveryAddress.streetNumber"
            v-model:postcode="form.deliveryAddress.postcode"
            v-model:city="form.deliveryAddress.city"
        />
        
        <AddressFieldGroup
            label="Billing address"
            v-model:street="form.billingAddress.street"
            v-model:streetNumber="form.billingAddress.streetNumber"
            v-model:postcode="form.billingAddress.postcode"
            v-model:city="form.billingAddress.city"
        />

    </form>
</template>

<script>
import {ref} from 'vue';
import AddressFieldGroup from './AddressFieldGroup.vue';

export default {
    components: {AddressFieldGroup},
    setup() {
        const form = ref({
            firstName: '321321',
            lastName: '321321',
            email: '321321',
            deliveryAddress: {
                street: '321321',
                streetNumber: '321',
                postcode: '321',
                city: '321',
            },
            billingAddress: {
                street: '',
                streetNumber: '',
                postcode: '',
                city: '',
            },
        });

        return {
            form,
        };
    },
};
</script>

On gists

03 copy to clipboard

cdruc vue

CopyToClipBoard.vue #

<script>
export default {
  data() {
    return {
      status: 'idle',
    };
  },
  methods: {
    copy(text) {
      // create textarea
      const el = document.createElement('textarea');
      
      // assign value
      el.value = text;
      
      // style textarea
      el.style.position = 'absolute';
      el.style.left = '-90000px';
      document.body.appendChild(el);

      // select its contents
      el.select();
      
      // execute copy
      document.execCommand("copy");
      
      // remove textarea
      document.body.removeChild(el);

      this.status = "copied";
      setTimeout(() => this.status = "idle", 1000);
    },
  },
  render() {
    return this.$slots.default({
      status: this.status,
      copy: this.copy
    });
  }
};
</script>

On gists

Promise.all

JavaScript

examples.js #

// Parallel
async function getData() {
    const [user, posts] = await Promise.all([
        fetchUserData(),
        fetchPostsData()
    ]);
    console.log(user, posts);
}


// non parallel
function getData() {
    return Promise.all([fetchUser(), fetchPosts()])
        .then(([user, posts]) => {
            console.log(user, posts);
        })
        .catch((error) => {
            console.error(error); 
        });
}

On gists

Content from Slot

Vue.js

TheTest.vue #

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

const test = ref()

onMounted(() => {
// 1
console.log(test.value.children[0])
 
 // 2 nebo bez refu primo ze slotu useSlots().default()[0].el
})
</script>

<template>
  <!-- nejde ref umistit primo na slot, nutno to takto obalit, nebo viz druhy zpusob -->
  <div ref="test"> 
  <slot />
  </div>
</template>

On gists

Dynamic variants

Tailwind CSS

tailwind.config.js #

// https://tailwindcss.com/docs/plugins#static-variants
// https://tallpad.com/series/tailwind/lessons/styling-vue-components-using-tailwindcss-custom-variants
// https://play.tailwindcss.com/8Pmxm8TomW


const plugin = require('tailwindcss/plugin')

/** @type {import('tailwindcss').Config} */
export default {
  theme: {
    extend: {
      // ...
    },
  },
  plugins: [
    plugin(function ({ addVariant }) {
      addVariant('hocus', ['&:hover', '&:focus'])
      addVariant('error', ['&[data-error=true]', '[data-error=true] &'])
    }),

  ],
}

On gists

Dynamic components

Tailwind CSS

index.html #

<!--
https://github.com/tailwindlabs/tailwindcss-aspect-ratio/blob/master/src/index.js#L39
https://play.tailwindcss.com/4cuLPqCv05
https://www.youtube.com/watch?v=sxxUK0l8jKE
-->

<div class="p-20 flex justify-center items-center gap-12">
  <img class="avatar-sm" src="https://res.cloudinary.com/thirus/image/upload/v1705061543/images/avatar.png" alt="" />
  <img class="avatar" src="https://res.cloudinary.com/thirus/image/upload/v1705061543/images/avatar.png" alt="" />
  <img class="avatar-lg" src="https://res.cloudinary.com/thirus/image/upload/v1705061543/images/avatar.png" alt="" />
  <img class="avatar-xl" src="https://res.cloudinary.com/thirus/image/upload/v1705061543/images/avatar.png" alt="" />
</div>

On gists

Subgrid examples by Thirus (Rows and columns)

Tailwind CSS CSS CSS trick

example1.html #

<!-- https://play.tailwindcss.com/CF9WdM43Jf -->

<section class="bg-slate-900 min-h-screen text-slate-100">

    <!-- Shopping Cart -->
    <div class="rounded-md bg-slate-800 p-8 grid grid-cols-[auto_1fr_auto_auto_auto] gap-6">
        <div class="grid gap-6 col-span-5 grid-cols-subgrid">
            <p class="col-span-2">Product</p>
            <p>Price</p>
            <p>Quantity</p>
            <p class="text-right">Total</p>
        </div>

        <div class="grid gap-6 col-span-5 grid-cols-subgrid">
            <img
                class="size-20 object-cover"
                src="https://tinyurl.com/3r25tr36"
                alt=""
            />
            <div>
                <h3 class="text-xl font-medium">
                    Stylish Tote Bag
                </h3>
                <p class="text-sm text-slate-400">
                    Women's Tote Bag Brown
                </p>
                <span class="text-sm text-slate-500">
                    #368798
                </span>
            </div>
            <p class="text-slate-400">
                $99.00
            </p>
            <label>
                <input
                    class="border border-slate-600 bg-transparent px-2 py-1 text-sm text-slate-400"
                    type="text"
                    value="1"
                    size="2"
                />
            </label>
            <p class="font-medium text-right">
                $99.00
            </p>
        </div>

        <div class="grid gap-6 col-span-5 grid-cols-subgrid">
            <img
                class="size-20 object-cover"
                src="https://tinyurl.com/3pj5teex"
                alt=""
            />
            <div>
                <h3 class="text-xl font-medium">
                    Sunglasses
                </h3>
                <p class="text-sm text-slate-400">
                    Wooden Frame
                </p>
                <span class="text-sm text-slate-500">
                    #756328
                </span>
            </div>
            <p class="text-slate-400">
                $102.00
            </p>
            <label>
                <input
                    class="border border-slate-600 bg-transparent px-2 py-1 text-sm text-slate-400"
                    type="text"
                    value="10"
                    size="2" />
            </label>
            <p class="font-medium text-right">
                $1020.00
            </p>
        </div>
    </div>
</section>