I'm trying to use a property of my data in a computed method like this:
data() {
return {
ToDoItems: [
{ id: uniqueId("todo-"), label: "Learn Vue", done: false },
{
id: uniqueId("todo-"),
label: "Create a Vue project with the CLI",
done: true,
},
{ id: uniqueId("todo-"), label: "Have fun", done: true },
{ id: uniqueId("todo-"), label: "Create a to-do list", done: false },
],
};
},
computed: {
listSummary() {
const numberFinishedItems = this.ToDoItems.filter((item) => item.done)
.length;
return `${numberFinishedItems} out of ${this.ToDoItems.length} items completed`;
},
},
But the IDE (Visual Studio Code) and the compiler throw an error:
Property 'ToDoItems' does not exist on type 'ComponentPublicInstance<{}, {}, {}, {}, {}, EmitsOptions, {}, {}, false, ComponentOptionsBase<{}, {}, {}, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, EmitsOptions, string, {}>>'.
I'm following the vue.js tutorial of mozilla (https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Client-side_JavaScript_frameworks/Vue_computed_properties#adding_a_summary_counter) but using v3.
Has anything changed that this isn't possible anymore / differently?
Thanks in advance
complete code:
<template>
<div id="app">
<h1>To-Do List</h1>
<to-do-form #todo-added="addToDo"></to-do-form>
<h2 id="list-summary">{{ listSummary }}</h2>
<ul aria-labelledby="list-summary" class="stack-large">
<li v-for="item in ToDoItems" :key="item.id">
<to-do-item :label="item.label" :done="true" :id="item.id"></to-do-item>
</li>
</ul>
</div>
</template>
<script lang="ts">
import uniqueId from "lodash.uniqueid";
import { defineComponent } from "vue";
import ToDoItem from "./components/ToDoItem.vue";
import ToDoForm from "./components/ToDoForm.vue";
export default defineComponent({
name: "App",
components: {
ToDoItem,
ToDoForm,
},
data() {
return {
ToDoItems: [
{ id: uniqueId("todo-"), label: "Learn Vue", done: false },
{
id: uniqueId("todo-"),
label: "Create a Vue project with the CLI",
done: true,
},
{ id: uniqueId("todo-"), label: "Have fun", done: true },
{ id: uniqueId("todo-"), label: "Create a to-do list", done: false },
],
};
},
methods: {
addToDo(toDoLabel: string) {
this.ToDoItems.push({
id: uniqueId("todo-"),
label: toDoLabel,
done: false,
});
},
},
computed: {
listSummary() {
const numberFinishedItems = this.ToDoItems.filter((item) => item.done)
.length;
return `${numberFinishedItems} out of ${this.ToDoItems.length} items completed`;
},
},
});
</script>
<style>
/* Global styles */
.btn {
padding: 0.8rem 1rem 0.7rem;
border: 0.2rem solid #4d4d4d;
cursor: pointer;
text-transform: capitalize;
}
.btn__danger {
color: #fff;
background-color: #ca3c3c;
border-color: #bd2130;
}
.btn__filter {
border-color: lightgrey;
}
.btn__danger:focus {
outline-color: #c82333;
}
.btn__primary {
color: #fff;
background-color: #000;
}
.btn-group {
display: flex;
justify-content: space-between;
}
.btn-group > * {
flex: 1 1 auto;
}
.btn-group > * + * {
margin-left: 0.8rem;
}
.label-wrapper {
margin: 0;
flex: 0 0 100%;
text-align: center;
}
[class*="__lg"] {
display: inline-block;
width: 100%;
font-size: 1.9rem;
}
[class*="__lg"]:not(:last-child) {
margin-bottom: 1rem;
}
#media screen and (min-width: 620px) {
[class*="__lg"] {
font-size: 2.4rem;
}
}
.visually-hidden {
position: absolute;
height: 1px;
width: 1px;
overflow: hidden;
clip: rect(1px 1px 1px 1px);
clip: rect(1px, 1px, 1px, 1px);
clip-path: rect(1px, 1px, 1px, 1px);
white-space: nowrap;
}
[class*="stack"] > * {
margin-top: 0;
margin-bottom: 0;
}
.stack-small > * + * {
margin-top: 1.25rem;
}
.stack-large > * + * {
margin-top: 2.5rem;
}
#media screen and (min-width: 550px) {
.stack-small > * + * {
margin-top: 1.4rem;
}
.stack-large > * + * {
margin-top: 2.8rem;
}
}
/* End global styles */
#app {
background: #fff;
margin: 2rem 0 4rem 0;
padding: 1rem;
padding-top: 0;
position: relative;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 2.5rem 5rem 0 rgba(0, 0, 0, 0.1);
}
#media screen and (min-width: 550px) {
#app {
padding: 4rem;
}
}
#app > * {
max-width: 50rem;
margin-left: auto;
margin-right: auto;
}
#app > form {
max-width: 100%;
}
#app h1 {
display: block;
min-width: 100%;
width: 100%;
text-align: center;
margin: 0;
margin-bottom: 1rem;
}
</style>
Boussadjra Brahim's answer is "alright" but it doesn't actually address the issue. user16362509 is correct, but doesn't answer the question.. What you need to do is yes, annotate the return types of both your data properties and computed properties so TS knows what's going on. Not only that, but it provides stricter type checking if you annotate all types. I don't believe this issue actually occurs when properly using Vue3's composition API, but the problem does, as you can see, exist in options Api. Try: (Or consider using Composition Api). Weirdly enough, when you annotate the return type of a computed method, it understands the context, just quirks of Vue but it's good practice to have the types explicit.
<script lang="ts">
import uniqueId from "lodash.uniqueid";
import { defineComponent } from "vue";
import ToDoItem from "./components/ToDoItem.vue";
import ToDoForm from "./components/ToDoForm.vue";
export default defineComponent({
name: "App",
components: {
ToDoItem,
ToDoForm,
},
data(): { ToDoItems: Array<{id: *string*, label: string, done: boolean}> } {
return {
ToDoItems: [
{ id: uniqueId("todo-"), label: "Learn Vue", done: false },
{
id: uniqueId("todo-"),
label: "Create a Vue project with the CLI",
done: true,
},
{ id: uniqueId("todo-"), label: "Have fun", done: true },
{ id: uniqueId("todo-"), label: "Create a to-do list", done: false },
],
};
},
methods: {
addToDo(toDoLabel: string): void {
this.ToDoItems.push({
id: uniqueId("todo-"),
label: toDoLabel,
done: false,
});
},
},
computed: {
listSummary(): string {
const numberFinishedItems = this.ToDoItems.filter((item) => item.done)
.length;
return `${numberFinishedItems} out of ${this.ToDoItems.length} items completed`;
},
},
});
</script>
You are already using Vue 3. Why not use composition API with script setup for even better typescript support?
Live demo
<template>
<div id="app">
<h1>To-Do List</h1>
<form #submit.prevent="addToDo">
<input type="text" ref="label" />
<button type="submit">Add</button>
</form>
<h2 id="list-summary">{{ listSummary }}</h2>
<ul aria-labelledby="list-summary" class="stack-large">
<li v-for="item in ToDoItems" :key="item.id">
<input type="checkbox" v-model="item.done" />
{{ item.id }} {{ item.label }}
</li>
</ul>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
interface ToDoItem {
id: string;
label: string;
done: boolean;
}
const label = ref(null)
const ToDoItems = ref<ToDoItem[]>([
{ id: 1, label: 'Learn Vue', done: false },
{ id: 2, label: 'Create a Vue project with the CLI', done: true },
{ id: 3, label: 'Have fun', done: true },
{ id: 4, label: 'Create a to-do list', done: false },
]);
const addToDo = () => {
ToDoItems.value.push({
id: ToDoItems.value.length + 1,
label: label.value.value,
done: false,
});
label.value.value = '';
};
const listSummary = computed(() => {
return `${ToDoItems.value.filter((item) => item.done).length} out of ${ToDoItems.value.length} items completed`;
});
</script>
Related
Created a small project in vuejs to understand how binding works with classes and style and so on.
The behavior I wanted is that when clicking on different dynamic buttons they should have different backgrounds and it works partially. The issue is when I clicked on one button the other buttons also change their background-color at the same time and I don't want this behavior.
I have active props and I think it's causing this issue.
DynamicButton.vue
<template>
<div>
<div class="btn1">
<button
v-on="$listeners"
:class="[dark ? 'dark' : 'light', 'baseButton']"
class="btn"
:style="{ backgroundColor: color }"
>
{{ buttonText }}
</button>
</div>
</div>
</template>
<script>
export default {
name: "DynamicButton",
props: {
buttonText: {
type: String,
default: "label",
},
dark: {
type: Boolean,
default: false,
},
light: {
type: Boolean,
default: true,
},
active: {
type: Boolean,
default: false,
},
color: {
type: String,
default: "gray",
},
},
};
</script>
<style scoped>
.baseButton {
border-radius: 5px;
border: none;
padding: 10px;
width: 200px;
height: 30px;
}
.light {
background: white;
color: black;
border: 1px solid lightgray;
}
.dark {
background: black;
color: white;
}
.btn {
margin: 10px;
}
</style>
app.vue
<template>
<div id="app">
<img alt="Vue logo" src="./assets/logo.png" />
<DynamicButton
buttonText="Dark Mode"
:dark="true"
#click="handleDarkMode"
:color="active ? 'red' : 'blue'"
/>
<DynamicButton
buttonText="Light Mode"
#click="handleLightMode"
:color="active ? this.color : '#16a085'"
/>
</div>
</template>
<script>
import DynamicButton from "./components/DynamicButton.vue";
export default {
name: "App",
components: {
HelloWorld,
DynamicButton,
},
props: {
// darkColorChange: {
// type: String,
// default: "",
// },
// lightColorChange: {
// type: String,
// default: "",
// },
},
data() {
return {
active: true,
color: "#3aa1b6",
};
},
methods: {
handleDarkMode() {
console.log("Dark-mode clicked");
// eslint-disable-next-line
// this.darkColorChange.style.backgroundColor = "pink";
this.active = !this.active;
},
handleLightMode() {
console.log("Light-mode clicked");
this.active = !this.active;
},
},
};
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
my buttons Components
The problem is that you are using the same variable active for two different components, so it's only normal that both components would react the same way.
I would go for two different variables or an array of objects, containing the data for all the buttons because you may have more buttons in the future.
I get errors in the console about the lack of the required props (height / width). How can I find out where to specify them (in what file and what should the class be called)?
I am aware that could be a dummy question so I also would be grateful if you would reccomend some viedo/tutorial in your opinion useful to get to know more devtools basics.
This is my first question, so I'm not sure what information I should provide. Let me know if anything is missing.
My console 1
Home.vue
<template>
<div id="home">
<LazyHydrate when-idle>
<SfHero class="hero">
<SfHeroItem
v-for="(hero, i) in heroes"
:key="i"
:title="$t(hero.title)"
:subtitle="$t(hero.subtitle)"
:background="hero.background"
:image="hero.image | addBasePathFilter"
:class="hero.className"
:height="200"
:width="200"
/>
</SfHero>
</LazyHydrate>
<LazyHydrate when-visible>
<SfBannerGrid :banner-grid="1" class="banner-grid">
<template v-for="item in banners" v-slot:[item.slot]>
<SfBanner
:key="item.slot"
:title="$t(item.title)"
:subtitle="$t(item.subtitle)"
:description="$t(item.description)"
:button-text="$t(item.buttonText)"
:link="localePath(item.link)"
:image="item.image | addBasePathFilter"
:class="item.class"
:height="200"
:width="200"
/>
</template>
</SfBannerGrid>
</LazyHydrate>
<LazyHydrate when-visible>
<div class="similar-products">
<SfHeading :title="$t('Match with it')" :level="2" />
<nuxt-link :to="localePath('/c/women')" class="smartphone-only">See all</nuxt-link>
</div>
</LazyHydrate>
<LazyHydrate when-visible>
<SfCarousel
class="carousel"
:settings="{ peek: 16, breakpoints: { 1023: { peek: 0, perView: 2 } } }"
>
<template #prev="{go}">
<SfArrow
aria-label="prev"
class="sf-arrow--left sf-arrow--long"
#click="go('prev')"
/>
</template>
<template #next="{go}">
<SfArrow
aria-label="next"
class="sf-arrow--right sf-arrow--long"
#click="go('next')"
/>
</template>
<SfCarouselItem
class="carousel__item"
v-for="(product, i) in products"
:key="i"
>
<SfProductCard
class="carousel__item__product"
:title="product._name"
:image="productGetters.getCoverImage(product) | addBasePathFilter"
image-tag="nuxt-img"
:nuxt-img-config="{
format: 'webp',
fit: 'fill'
}"
:image-width="216"
:image-height="290"
:regular-price="productPriceTransform(product).regular"
:special-price="productPriceTransform(product).special"
:is-added-to-cart="isInCart({ product })"
:is-in-wishlist="isInWishlist({ product })"
show-add-to-cart-button
:link="localePath(`/p/${productGetters.getSlug(product)}/${productGetters.getSku(product)}`)"
#click:add-to-cart="addToCart({ product, quantity: 1 })"
#click:wishlist="!isInWishlist({ product }) ? addProductToWishlist(product) : removeProductFromWishlist(product)"
/>
</SfCarouselItem>
</SfCarousel>
</LazyHydrate>
<LazyHydrate when-visible>
<SfCallToAction
:title="$t('Subscribe to Newsletters')"
:button-text="$t('Subscribe')"
:description="$t('Be aware of upcoming sales and events. Receive gifts and special offers!')"
:image="'/homepage/newsletter.webp' | addBasePathFilter"
class="call-to-action"
>
<template #button>
<SfButton
class="sf-call-to-action__button"
data-testid="cta-button"
#click="handleNewsletterClick"
>
{{ $t('Subscribe') }}
</SfButton>
</template>
</SfCallToAction>
</LazyHydrate>
<LazyHydrate when-visible>
<NewsletterModal #email-submitted="onSubscribe" />
</LazyHydrate>
</div>
</template>
<script>
import {
SfHero,
SfBanner,
SfCallToAction,
SfCarousel,
SfProductCard,
SfBannerGrid,
SfHeading,
SfArrow,
SfButton
} from '#storefront-ui/vue';
import LazyHydrate from 'vue-lazy-hydration';
import { ref, computed, watch, useContext } from '#nuxtjs/composition-api';
import { onSSR } from '#vue-storefront/core';
import {
useCart,
useFacet,
useWishlist,
useCurrency,
facetGetters,
productGetters,
wishlistGetters,
productPriceTransform
} from '#vsf-enterprise/commercetools';
import NewsletterModal from '~/components/NewsletterModal.vue';
import { useUiState, useUiNotification } from '../composables';
export default {
name: 'Home',
setup() {
const { app: { i18n } } = useContext();
const { toggleNewsletterModal } = useUiState();
const { send } = useUiNotification();
const {
isInCart,
addItem: addItemToCart,
error: cartError
} = useCart();
const { result, search } = useFacet('home');
const { currency } = useCurrency();
const { addItem: addItemToWishlist, isInWishlist, removeItem: removeItemFromWishlist, wishlist, error: wishlistError } = useWishlist();
const products = computed(() => facetGetters.getProducts(result.value));
const fetchProducts = async () => {
await search({
filters: {},
page: 1,
itemsPerPage: 12,
sort: 'latest',
phrase: ''
});
};
watch(currency, async () => {
await fetchProducts();
});
onSSR(async () => {
await fetchProducts();
});
const mocks = {
heroes: [
{
title: 'Colorful summer dresses are already in store',
subtitle: 'SUMMER COLLECTION 2022',
background: '#eceff1',
image: '/homepage/bannerH.webp'
},
{
title: 'Colorful summer dresses are already in store',
subtitle: 'SUMMER COLLECTION 2022',
background: '#efebe9',
image: '/homepage/bannerA.webp',
className:
'sf-hero-item--position-bg-top-left sf-hero-item--align-right'
},
{
title: 'Colorful summer dresses are already in store',
subtitle: 'SUMMER COLLECTION 2022',
background: '#fce4ec',
image: '/homepage/bannerB.webp'
}
],
banners: [
{
slot: 'banner-A',
subtitle: 'Dresses',
title: 'Cocktail & Party',
description: 'Find stunning women\'s cocktail dresses and party dresses. Stand out in lace and metallic cocktail dresses from all your favorite brands.',
buttonText: 'Shop now',
image: '/homepage/bannerF.webp',
class: 'sf-banner--slim desktop-only',
link: '/c/women/women-clothing-skirts'
},
{
slot: 'banner-B',
subtitle: 'Dresses',
title: 'Linen Dresses',
description: 'Find stunning women\'s cocktail dresses and party dresses. Stand out in lace and metallic cocktail dresses from all your favorite brands.',
buttonText: 'Shop now',
image: '/homepage/bannerE.webp',
class: 'sf-banner--slim banner-central desktop-only',
link: '/c/women/women-clothing-dresses'
},
{
slot: 'banner-C',
subtitle: 'T-Shirts',
title: 'The Office Life',
image: '/homepage/bannerC.webp',
class: 'sf-banner--slim banner__tshirt',
link: '/c/women/women-clothing-shirts'
},
{
slot: 'banner-D',
subtitle: 'Summer Sandals',
title: 'Eco Sandals',
image: '/homepage/bannerG.webp',
class: 'sf-banner--slim',
link: '/c/women/women-shoes-sandals'
}
]
};
const heroes = ref(mocks.heroes);
const banners = ref(mocks.banners);
const handleNewsletterClick = () => {
toggleNewsletterModal();
};
const onSubscribe = (emailAddress) => {
console.log(`Email ${emailAddress} was added to newsletter.`);
toggleNewsletterModal();
};
const addToCart = async ({ product, quantity }) => {
const { id, sku } = product;
await addItemToCart({
product: { id, sku },
quantity
});
if (!cartError.value.addItem) {
send({
type: 'success',
message: i18n.t('Product has been added to the cart.')
});
}
};
const addProductToWishlist = async (product) => {
await addItemToWishlist({ product });
if (!wishlistError.value.addItem) {
send({
type: 'success',
message: i18n.t('Product has been added to the wishlist.')
});
}
};
const removeProductFromWishlist = async (productItem) => {
const productsInWhishlist = computed(() => wishlistGetters.getItems(wishlist.value));
const product = productsInWhishlist.value.find(wishlistProduct => wishlistProduct.variant.sku === productItem.sku);
await removeItemFromWishlist({ product });
if (!wishlistError.value.removeItem) {
send({
type: 'success',
message: i18n.t('Product has been removed from the wishlist.')
});
}
};
return {
heroes,
banners,
products,
productGetters,
handleNewsletterClick,
onSubscribe,
isInCart,
addToCart,
addProductToWishlist,
isInWishlist,
removeProductFromWishlist,
productPriceTransform
};
},
components: {
LazyHydrate,
NewsletterModal,
SfArrow,
SfBanner,
SfBannerGrid,
SfButton,
SfCallToAction,
SfCarousel,
SfHeading,
SfHero,
SfProductCard
},
beforeRouteEnter (_, _2, next) { next('/home-page') }
};
</script>
<style lang="scss">
.carousel__item__product {
.sf-product-card__title {
margin: var(--spacer-base) 0 var(--spacer-xs) 0;
}
.sf-product-card__add-button {
margin-bottom: var(--spacer-xl);
}
}
</style>
<style lang="scss" scoped>
#home {
box-sizing: border-box;
padding: 0 var(--spacer-sm);
#include for-desktop {
max-width: 1240px;
padding: 0;
margin: 0 auto;
}
}
.hero {
margin: var(--spacer-xl) auto var(--spacer-lg);
--hero-item-background-position: center;
#include for-desktop {
margin: var(--spacer-xl) auto var(--spacer-2xl);
}
.sf-hero-item {
min-height: 230px;
&:nth-child(even) {
--hero-item-background-position: left;
#include for-mobile {
--hero-item-background-position: 30%;
::v-deep .sf-hero-item__subtitle,
::v-deep .sf-hero-item__title {
text-align: right;
width: 100%;
padding-left: var(--spacer-sm);
}
}
}
}
::v-deep .sf-hero__control {
&--right,
&--left {
display: none;
}
}
}
.banner-grid {
--banner-container-width: 50%;
margin: var(--spacer-xl) 0;
::v-deep .sf-link:hover {
color: var(--c-white);
}
#include for-desktop {
margin: var(--spacer-2xl) 0;
::v-deep .sf-link {
--button-width: auto;
text-decoration: none;
}
}
}
.banner {
&__tshirt {
background-position: left;
}
&-central {
#include for-desktop {
--banner-container-flex: 0 0 70%;
}
}
}
.similar-products {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: var(--spacer-2xs);
--heading-padding: 0;
border-bottom: 1px var(--c-light) solid;
#include for-desktop {
border-bottom: 0;
justify-content: center;
padding-bottom: 0;
}
}
.call-to-action {
background-position: right;
margin: var(--spacer-xs) 0;
#include for-desktop {
margin: var(--spacer-xl) 0 var(--spacer-2xl) 0;
}
}
.carousel {
margin: 0 calc(0 - var(--spacer-sm)) 0 0;
#include for-desktop {
margin: 0;
}
&__item {
margin: 1.375rem 0 2.5rem 0;
#include for-desktop {
margin: var(--spacer-xl) 0 var(--spacer-xl) 0;
}
&__product {
--product-card-add-button-transform: translate3d(0, 30%, 0);
::v-deep .sf-product-card {
&__title {
margin: var(--spacer-base) 0 var(--spacer-xs) 0;
}
&__add-button {
margin-bottom: var(--spacer-xl);
}
}
}
}
::v-deep .sf-arrow--long .sf-arrow--right {
--arrow-icon-transform: rotate(180deg);
-webkit-transform-origin: center;
transform-origin: center;
}
}
</style>
default.vue
<template>
<div>
<RenderContent v-if="styleGuide.length" :content="styleGuide" />
<LazyHydrate when-visible>
<TopBar class="desktop-only" />
</LazyHydrate>
<LazyHydrate when-idle>
<AppHeader />
</LazyHydrate>
<div id="layout">
<nuxt :key="$route.fullPath" />
<LazyHydrate when-visible>
<BottomNavigation />
</LazyHydrate>
<CartSidebar />
<WishlistSidebar />
<FiltersSidebar />
<LoginModal />
<Notification />
</div>
<LazyHydrate when-visible>
<AppFooter />
</LazyHydrate>
</div>
</template>
<script>
import AppHeader from '~/components/AppHeader.vue'
import BottomNavigation from '~/components/BottomNavigation.vue'
import AppFooter from '~/components/AppFooter.vue'
import TopBar from '~/components/TopBar.vue'
import CartSidebar from '~/components/CartSidebar.vue'
import WishlistSidebar from '~/components/WishlistSidebar.vue'
import FiltersSidebar from '~/components/FiltersSidebar.vue'
import LoginModal from '~/components/LoginModal.vue'
import Notification from '~/components/Notification'
import useCmsLayout from '~/composables/useCmsLayout'
import { onMounted } from '#vue/composition-api'
import LazyHydrate from 'vue-lazy-hydration'
import { useStore, useUser, useWishlist } from '#vsf-enterprise/commercetools'
import { onSSR } from '#vue-storefront/core'
export default {
name: 'DefaultLayout',
components: {
LazyHydrate,
TopBar,
AppHeader,
BottomNavigation,
AppFooter,
CartSidebar,
WishlistSidebar,
FiltersSidebar,
LoginModal,
Notification,
},
setup() {
const { load: loadStores } = useStore()
const { load: loadUser } = useUser()
const { load: loadWishlist } = useWishlist()
const { getLayout, styleGuide } = useCmsLayout()
onSSR(async () => {
await Promise.all([loadStores(), getLayout()])
})
onMounted(async () => {
await Promise.all([loadUser(), loadWishlist()])
})
return {
styleGuide,
}
},
head() {
return this.$nuxtI18nHead({ addSeoAttributes: true })
},
}
</script>
<style lang="scss">
#import '~#storefront-ui/vue/styles';
#layout {
box-sizing: border-box;
#include for-desktop {
max-width: 1240px;
margin: auto;
}
}
.no-scroll {
overflow: hidden;
height: 100vh;
}
// Reset CSS
html {
width: auto;
#include for-mobile {
overflow-x: hidden;
}
}
body {
overflow-x: hidden;
color: var(--c-text);
font-size: var(--font-size--base);
font-family: var(--font-family--primary);
margin: 0;
padding: 0;
}
a {
text-decoration: none;
color: var(--c-link);
&:hover {
color: var(--c-link-hover);
}
}
h1 {
font-family: var(--font-family--secondary);
font-size: var(--h1-font-size);
line-height: 1.6;
margin: 0;
}
h2 {
font-family: var(--font-family--secondary);
font-size: var(--h2-font-size);
line-height: 1.6;
margin: 0;
}
h3 {
font-family: var(--font-family--secondary);
font-size: var(--h3-font-size);
line-height: 1.6;
margin: 0;
}
h4 {
font-family: var(--font-family--secondary);
font-size: var(--h4-font-size);
line-height: 1.6;
margin: 0;
}
</style>
Welcome on stackoverflow!
This error is triggered when a Vue component declares "props" as required, and they are not provided.
The logs says:
Missing required prop height. ------------------- SfImage.vue
So this error is trigger from your SfImage.vue component.
Hence I guess you declared the height and width props from this component as required.
Solution:
You remove the required: true option of your props
OR
You correctly provide these props when you use that component: <SfImage height="40px" width="40px" />
The error in the Chrome Dev Tools usually tells you the name of the component you used without providing the required props. In a vue component, props are declared inside it, under the props property (if using vue2), or under declareProps macro (if using vue3)
For more info, you can read about vue component props in the official documentation: https://vuejs.org/guide/components/props.html
How to align the width of the cells in the header and in the main part?
I marked the correct option in the picture with green checkmarks.
https://i.stack.imgur.com/PP1w2.png
My example and solution now: https://codepen.io/horus123/pen/YzVOGLQ
<div id="test">
<div class="table">
<div class="table__header">
<div v-for="(item,index) in headers" :key="index" class="table__head-el">
{{ item.title }}
</div>
</div>
<div class="table__body">
<div v-for="(el, indexx) in tableItems" :key="indexx" class="table__row">
<span v-for="(elem, indexxx) in el" :key="indexxx" class="table__field">
{{elem}}
</span>
</div>
</div>
</div>
One option would be to use the same grid for both the header and the main part. In other word the display: grid would be apply to the div.table element. In order to make div.table__head-el and div.table__field cells of this grid div.table__header, div.table__body and table__row must have display: contents. The rest of the CSS must be adapt as well but HTML and JS stay the same (I've added a blank property at the end of tableItems objects so the length of those items match the length of the header)
new Vue({
el: "#test",
data: {
headers: [
{
title: '#'
},
{
title: 'ID', icon: 'height'
},
{
title: 'Номер', icon: 'height'
},
{
title: 'Тип', icon: 'height'
},
{
title: 'Марка', icon: 'height'
},
{
title: 'Логист', icon: 'height'
},
{
title: 'Колонна', icon: 'height'
},
{
title: 'Трекер', icon: 'height'
},
{
title: 'Дата привязки трекера', icon: 'height'
},
{
title: 'Дата последних координат', icon: 'height'
},
{
title: 'Удалена'
},
{
title: 'Дата удаления'
}
],
tableItems: [
{
number: 1,
id: '42537370',
numberCar: 'В855АТ147',
type: 'Тягач',
brand: 'Mercedes-Benz',
logistician: 'Томсон Артём Александрович',
column: 'Андреев Евгений',
tracker: '86793',
dateStart: '29.03.2021 16:42:01',
dateEnd: '07.06.2021 13:49:39',
isDeleted: false,
blank: ''
},
{
number: 1,
id: '42537370',
numberCar: 'В855АТ147',
type: 'Тягач',
brand: 'Mercedes-Benz',
logistician: 'Имя Фамилия',
column: 'Андреев',
tracker: '48671111111193',
dateStart: '29.03.2021 16:42:01',
dateEnd: '07.06.2021 13:49:39',
isDeleted: false,
blank: ''
}
],
},
computed: {
},
methods: {
}
});
html {
--border: 1px solid black;
--border-radius: 8px;
}
.table {
max-width: 100%;
padding: 0 75px;
display: grid;
grid-template-columns: minmax(0, 60px) repeat(11, minmax(0, auto));
}
.table__header, .table__body, .table__row {
display: contents;
}
.table__head-el, .table__field {
padding: 12px 20px;
}
.table__head-el {
border-top: var(--border);
border-bottom: var(--border);
margin-bottom: 20px;
}
.table__head-el, .table__field {
display: grid;
place-items: center;
overflow: hidden;
}
.table__head-el:first-child {
border-left: var(--border);
border-radius: var(--border-radius) 0 0 var(--border-radius);
}
.table__head-el:last-child {
border-right: var(--border);
border-radius: 0 var(--border-radius) var(--border-radius) 0;
}
.table__row:first-child > .table__field {
border-top: var(--border);
}
.table__row:last-child > .table__field {
border-bottom: var(--border);
}
.table__field:first-child {
border-left: var(--border);
}
.table__field:last-child {
border-right: var(--border);
}
.table__row:first-child > .table__field:first-child {
border-top-left-radius: var(--border-radius);
}
.table__row:first-child > .table__field:last-child {
border-top-right-radius: var(--border-radius);
}
.table__row:last-child > .table__field:first-child {
border-bottom-left-radius: var(--border-radius);
}
.table__row:last-child > .table__field:last-child {
border-bottom-right-radius: var(--border-radius);
}
.table__row:hover > .table__field {
color: red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="test">
<div class="table">
<div class="table__header">
<div v-for="(item,index) in headers" :key="index" class="table__head-el">
{{ item.title }}
</div>
</div>
<div class="table__body">
<div v-for="(el, indexx) in tableItems" :key="indexx" class="table__row">
<span v-for="(elem, indexxx) in el" :key="indexxx" class="table__field">
{{elem}}
</span>
</div>
</div>
</div>
</div>
I'm new to vue and Nuxt and I have a task to deploy a static page with youtube api and search feature
So Everytime I try to generate a static page using nuxt it comes without my pics or youtube api
index.vue
<template>
<div class="iframe">
<input class="textR" type="text" v-model="searchInput">
<button class="button" style="vertical-align:middle" #click="search(searchInput)" ><span> Search</span></button>
<div v-if="videos!=null" id="iframeContainer" style="display: inline-table;">
<iframe class="overall" v-for="item in videos" :key="item.id.videoId" width="640" height="480"
:src="'https://www.youtube.com/embed/'+item.id.videoId"
frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen></iframe>
</div>
</div>
</template>
<script>
export default {
created() {
let axios = require('axios');
let params = {
part: 'snippet',
key: 'myApiCode',
q: 'كايروكى',
maxResults: 5,
type: 'video'
};
var parent = this;
axios.get('https://www.googleapis.com/youtube/v3/search', { params })
.then(response => {
parent.videos = response.data.items }
)
.catch(error => console.error(error));
},
mounted(){
setTimeout(function(){ let count = document.getElementById("iframeContainer").children;
if (count.length%2 != 0) {count[count.length -1].style.cssText = "margin: auto; display: table-cell;"};
},1000)
},
data() {
return {
videos: null,
loading: true,
searchInput: ""
}
},
methods: {
search(keyword){
let axios = require('axios');
let params = {
part: 'snippet',
key: 'AIzaSyBqJ9T8tven3Ge0_hNDWKjJ3lqOoL5N90s',
q: keyword,
maxResults: 5,
type: 'video'
};
var parent = this;
axios.get('https://www.googleapis.com/youtube/v3/search', { params })
.then(response => {
parent.videos = response.data.items }
)
.catch(error => console.error(error));
},
}
}
</script>
<style>
body {background-color: black;}
input[type=text] {
background-color: white;
background-image: url('../components/searchicon.png');
background-position: -1px 0px;
background-repeat: no-repeat;
padding-left: 30px;
background-size: 14%;
color : rgb(0, 0, 0);
}
.overall{
padding: 23px 20px;
background-color: #272727;
margin: 40px 50px;
border: 1px red solid;
}
.iframe {
padding: 23px 20px;
margin: 40px 100px;
}
.button {
display: inline-block;
border-radius: 4px;
background-color: #f4511e;
border: none;
color: #FFFFFF;
text-align: center;
font-size: 28px;
padding: 20px;
width: 200px;
transition: all 0.5s;
cursor: pointer;
margin: 5px;
}
.textR{
padding: 5px 5px;
margin: 20px 50px;
}
.button span {
cursor: pointer;
display: inline-block;
position: relative;
transition: 0.5s;
}
.button span:after {
content: '\00bb';
position: absolute;
opacity: 0;
top: 0;
right: -20px;
transition: 0.5s;
}
.button:hover span {
padding-right: 25px;
}
.button:hover span:after {
opacity: 1;
right: 0;
}
</style>
nuxt.config.js
import colors from 'vuetify/es5/util/colors'
// const routerBase = process.env.DEPLOY_ENV === 'master' ? {
// router: {
// base: '/youtube-task/'
// }
// } : {}
export default {
// ...routerBase,
mode: 'universal',
/*
** Headers of the page
*/
head: {
titleTemplate: '%s - ' + process.env.npm_package_name,
title: process.env.npm_package_name || '',
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ hid: 'description', name: 'description', content: process.env.npm_package_description || '' }
],
link: [
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
]
},
// router: {
// base: '/youtube-task/'
// },
/*
** Customize the progress-bar color
*/
loading: { color: '#fff' },
/*
** Global CSS
*/
css: [
],
/*
** Plugins to load before mounting the App
*/
plugins: [
],
/*
** Nuxt.js dev-modules
*/
buildModules: [
'#nuxtjs/vuetify',
],
/*
** Nuxt.js modules
*/
modules: [
// Doc: https://axios.nuxtjs.org/usage
'#nuxtjs/axios',
],
/*
** Axios module configuration
** See https://axios.nuxtjs.org/options
*/
axios: {
},
/*
** vuetify module configuration
** https://github.com/nuxt-community/vuetify-module
*/
vuetify: {
customVariables: ['~/assets/variables.scss'],
theme: {
dark: true,
themes: {
dark: {
primary: colors.blue.darken2,
accent: colors.grey.darken3,
secondary: colors.amber.darken3,
info: colors.teal.lighten1,
warning: colors.amber.base,
error: colors.deepOrange.accent4,
success: colors.green.accent3
}
}
}
},
/*
** Build configuration
*/
build: {
/*
** You can extend webpack config here
*/
extend (config, ctx) {
}
}
}
package.json
{
"name": "youtube-task",
"version": "1.0.0",
"description": "My flawless Nuxt.js project",
"author": "MostafaDesoky",
"private": true,
"scripts": {
"dev": "nuxt",
"build": "nuxt build",
"start": "nuxt start",
"generate": "nuxt generate",
"build:gh-pages": "cross-env DEPLOY_ENV=master nuxt build",
"generate:gh-pages": "cross-env DEPLOY_ENV=master nuxt generate",
"deploy": "push-dir --dir=dist --branch=master --cleanup"
},
"dependencies": {
"nuxt": "^2.0.0",
"#nuxtjs/axios": "^5.3.6"
},
"devDependencies": {
"#nuxtjs/vuetify": "^1.0.0",
"cross-env": "^7.0.2",
"push-dir": "^0.4.1"
}
}
Anyhelp would be great I tried alot of methods but nothing
I tried to make netlify generate the dist for me and it was the same with no pic nor youtube api
Edit #1 : When I run it on my localhost it works perfectly through npm run dev,
the problem when I try to generate the dist folder and upload it to anywhere
Have you tried to enable spa mode before generating static website ?
mode: 'spa',
And why don't you use the axios helper directly instead of requiring it ?
this.$axios.get('https://www.googleapis.com/youtube/v3/search', { params })
For your background images you have to use this syntax :
background-image: url('~folder/nameoffile.ext');
In Vue, what should I do when I want to click a component to add styles to this component and to clean up the additional styles that have been clicked on other tabbox components before? Thanks in advance!
Here is the code of the sub component
<template>
<div :class="tabStyle" :style="boxstyle" #click="tabClick(name)">
{{name}}
<div class="selected-icon" v-show="isSelected"></div> <!--selected styles-->
<div class="tick" v-show="isSelected"></div> <!--selected styles-->
</div>
</template>
<script>
export default {
name: "tabbox",
props: {
name: {
type: String,
default: ""
},
boxstyle: {
type: Object,
defalult: {}
}
},
data() {
return {
isSelected: false,
tabStyle: {
"selected-box": false,
"unselected-box": true
}
};
},
methods: {
tabClick(name) {
this.isSelected = true;
this.borderChange("selected-box","unselected-box")//style add
this.$emit("getTabName", name);
},
borderChange(first, second) {
this.tabStyle[first] = true;
this.tabStyle[second] = false;
}
}
};
</script>
<style lang="scss" scoped>
.tab-box {
display: inline-block;
position: relative;
text-align: center;
padding: 1%;
font-size: 1rem;
width: 20%;
}
.unselected-box {
border: solid 1px #b9a7a76b;
#extend .tab-box;
}
.selected-box {
border: solid 1px #5ddb14;
#extend .tab-box;
}
.selected-icon {
position: absolute;
right: 0;
bottom: 0;
width: 0;
height: 0;
border-color: #5ddb14 transparent;
border-width: 0 0 20px 25px;
border-style: solid;
}
.tick {
position: absolute;
right: 0;
bottom: 0;
color: #fff;
&::after {
content: "✓";
}
}
</style>
And This is the code of the parent component
<template>
<div class="select-tab" :style="tabStyle">
<Header></Header>
<div class="label-content" v-for="(item,index) in categories" :key="index">
<meaning-label :name="item.name"></meaning-label>
<div class="box-content">
<TabBox #getTabName="getTabName" :name="_item.name" :boxstyle="styles" v-for="(_item,_index) in item.categoryList" :key="_index">
</TabBox>
</div>
</div>
</div>
</template>
<script>
import TabBox from "#/components/FindMusic/SelectTab/TabBox";
import MeaningLabel from "#/components/FindMusic/SelectTab/MeaningLabel";
import Header from "#/components/FindMusic/SelectTab/Header";
export default {
components: {
TabBox,
MeaningLabel,
Header
},
methods: {},
data() {
return {
styles: {
width: ""
},
allStyles: {
width: "94%",
margin: "2px 1.5%"
},
_categories: {}
};
},
mounted() {
this.categories = this.$store.state.CategoriesInfo.categories;
},
props: {
tabStyle: {
type: Object,
default: {}
},
categories: {
type:Array,
default: []
}
},
methods: {
getTabName(name){
this.$emit('getTabName',name)
}
}
};
</script>
<style lang="scss" scoped>
.box-content {
display: inline-block;
vertical-align: top;
width: 75%;
font-size: 0;
}
.label-content {
margin-top: 10px;
}
</style>
Just keep the style on the tab i click right now and remove the style i has clicked before .
One possible solution is get boxstyle from a Vue method:
<template>
<div class="select-tab" :style="tabStyle">
<Header></Header>
<div class="label-content" v-for="(item,index) in categories" :key="index">
<meaning-label :name="item.name"></meaning-label>
<div class="box-content">
<TabBox #getTabName="getTabName" :name="_item.name" :boxstyle="getTableStyle(_item.name)" v-for="(_item,_index) in item.categoryList" :key="_index">
</TabBox>
</div>
</div>
</div>
</template>
<script>
import TabBox from "#/components/FindMusic/SelectTab/TabBox";
import MeaningLabel from "#/components/FindMusic/SelectTab/MeaningLabel";
import Header from "#/components/FindMusic/SelectTab/Header";
export default {
components: {
TabBox,
MeaningLabel,
Header
},
methods: {},
data() {
return {
styles: {
width: ""
},
allStyles: {
width: "94%",
margin: "2px 1.5%"
},
_categories: {},
activeTabName: ''
};
},
mounted() {
this.categories = this.$store.state.CategoriesInfo.categories;
// you might want to set default activeTabName here or in Vue's watch
},
props: {
tabStyle: {
type: Object,
default: {}
},
categories: {
type:Array,
default: []
}
},
methods: {
getTabName(name){
this.$emit('getTabName',name)
this.activeTabName = name
},
getTableStyle (name) {
if (name === this.activeTabName) {
return this.allStyles
}
return {}
}
}
};
</script>
<style lang="scss" scoped>
.box-content {
display: inline-block;
vertical-align: top;
width: 75%;
font-size: 0;
}
.label-content {
margin-top: 10px;
}
</style>