So I have several components <server-header>. It has the following HTML:
<span #click="$parent.changeOrder(column, $event)">
<slot></slot>
<i v-show="sortActive" class="order-arrow" :class="sort"></i>
</span>
These components are inserted in another component <server-list>. These will be the headers, that when clicked, will order some lists. My objective is to only show one arrow icon at a time.
E.g.: If I click on the first header, the arrow appears on that one. If I click on the second header, the arrow from the first header hides and the one on the second header appears.
This would be simple to do with jQuery for example, but I'm kind of lost on how to do this with VueJS.
Don't call parent functions directly. That is an anti pattern. Instead use 2-way data binding with sync and this is much easier. Rough example below:
// server-list.vue
data() {
return {
selected: undefined
}
}
<server-header v-for="(header, idx) in headers" :header="header" :selected.sync="selected" :idx="idx"></server-header
Then in the child, we drop #click="$parent.changeOrder(column, $event)" in favor of:
#click="$emit('update:selected', idx)"
Make sure server-header has this prop expected:
props: ['idx', 'header', 'selected']
Then make sure we compare the idx to our header index:
<i v-show="selected === index"
I assume you're rendering the <server-header> components with a v-for directive, looping a server_headers property in your <server-list> component data property.
The way i go with this is adding a prop to the <server-header> component like selected, then I add a selected property to each of the server_headers objects. Then i render the icon (<i>) with v-show if the correspondent selected property is true.
Like this:
server-list component
Vue.component('server-list', {
data: () => ({
server_headers: [{key: value, selected: true}, {key:value, selected:false}]
}),
template: `
<div>
<server-header v-for="header of server_headers" :v-key="somekey" :selected="header.selected"></server_header>
</div>
`
}
Vue.component('server-header', {
props: ['selected'],
template: `
<span #click="$parent.changeOrder(column, $event)">
<slot></slot>
<i v-show="selected" class="order-arrow" :class="sort"></i>
</span>
`
})
Then, if i click on the arrow I would unset all the selected properties, and set the one i clicked. I hope this helps!!!!
Related
I'm currently building an app using the Vue framework and came across a strange issue that I was unable to find a great solution for so far:
What I'm trying to do is add a class to a parent container in case a specific element inside the container (input, select, textarea etc.) gets focus. Here's the example code:
<div class="form-group placeholder-label">
<label for="desc"><span>Description</span></label>
<div class="input">
<input id="desc" type="text" />
</div>
</div>
In Vanilla JS of course, this is easily done:
const parent = document.querySelector('.placeholder-label');
const input = parent.querySelector('input');
input.addEventListener('focus', (e) => {
parent.classList.add('active');
});
In the same way, you could loop through all .placeholder-label elements and add the event to their child inputs/selects etc. to add this basic functionality. There are two moving parts here:
You don't know the type of the parent element, just that it has .placeholder-label on it.
You don't know the type of the child element, just that it is some sort of HTML form element inside the parent element.
Can I build a Vue component that toggles a class on a given parent element based on focus/blur of a given child element? The best I could come up with is use slots for the child elements, but then I still need to build a component for each parent. Even when using mixins for the reused parts it's still quite a mess compared to the five lines of code I need to write in pure JS.
My template:
<template>
<div
class="form-group"
:class="{ 'active': active }"
>
<label :for="inputID"><span>{{ inputLabel }}</span></label>
<slot
name="input"
:focusFunc="makeActive"
:blurFunc="makeInactive"
:inputLabel="inputLabel"
:inputID="inputID"
/>
</div>
</template>
<script>
export default {
name: 'TestInput',
props: {
inputLabel: {
type: String,
default: '',
},
inputID: {
type: String,
required: true,
},
},
// data could be put into a mixin
data() {
return {
active: false,
};
},
// methods could be put into a mixin
methods: {
makeActive() {
this.active = true;
},
makeInactive() {
this.active = false;
},
},
};
</script>
Usage:
<test-input
:input-i-d="'input-2'"
:input-label="'Description'"
>
<template v-slot:input="scopeVars">
<!-- this is a bootstrap vue input component -->
<b-form-input
:id="scopeVars.inputID"
:state="false"
:placeholder="scopeVars.inputLabel"
#blur="scopeVars.blurFunc"
#focus="scopeVars.focusFunc"
/>
</template>
</test-input>
I guess I'm simply missing something or is this a problem that Vue just can't solve elegantly?
Edit: In case you're looking for an approach to bubble events, here you go. I don't think this works with slots however, which is necessary to solve my issue with components.
For those wondering here are two solutions. Seems like I did overthink the issue a bit with slots and everything. Initially I felt like building a component for a given element that receives a class based on a given child element's focus was a bit too much. Turns out it indeed is and you can easily solve this within the template or css.
CSS: Thanks to #Davide Castellini for bringing up the :focus-within pseudo-selector. I haven't heard of that one before. It works on newer browsers and has a polyfill available.
TEMPLATE I wrote a small custom directive that can be applied to the child element and handles everything.
Usage:
v-toggle-parent-class="{ selector: '.placeholder-label', className: 'active' }"
Directive:
directives: {
toggleParentClass: {
inserted(el, { value }) {
const parent = el.closest(value.selector);
if (parent !== null) {
el.addEventListener('focus', () => {
parent.classList.add(value.className);
});
el.addEventListener('blur', () => {
parent.classList.remove(value.className);
});
}
},
},
},
try using $emit
child:
<input v-on:keyup="emitToParent" />
-------
methods: {
emitToParent (event) {
this.$emit('childToParent', this.childMessage)
}
}
Parent:
<child v-on:childToParent="onChildClick">
--------
methods: {
// Triggered when `childToParent` event is emitted by the child.
onChildClick (value) {
this.fromChild = value
}
}
use this pattern to set a property that you use to change the class
hope this helps. let me know if I misunderstood or need to better explain!
I am turning some of my components into re-usable components. I am running into some issues here that I can't figure out. Coming from a React environment, my thoughts are getting jammed up. Basically, I need to be able to make a prop more versatile than just a Boolean or String, or any primitive value. I need to be able to pass "content" to it that could change from page to page depending on what is used for
For example, I have this stateless component:
<template>
<div class="cts-split-grid cts-alt-header">
<div>{{title}}</div>
<div v-if="rightSide" class="cts-split-grid">
<span class="uk-text-small">Pod or station is open</span>
<span class="legend-color"></span>
</div>
</div>
</template>
<script>
export default {
name: "page-alt-header",
props: {
title: {
type: String
},
rightSide: {
type: Boolean
}
},
data() {
return {
value: ""
};
}
};
</script>
That I am using this way
<AltHeader :title="'POD' + currentPodId" rightSide />
As you can see, in the title I am passing an object currentPodId bounded to the component. That was easy since that object only produces a data value.
I want to remove this(below) from the re-usable component and be able to add it in the component using the AltHeader as a rightSide Prop:
<span class="uk-text-small">Pod or station is open</span>
<span class="legend-color"></span>
The reason why is because this component's right side can be anything from an Icon component to a button, to a small block of HTML, etc.
How can I do this? How can I set up rightSide prop to accept anything I pass to it at the component level depending on how I need to use it?
Thanks
You should use slots
<template>
<div class="cts-split-grid cts-alt-header">
<div>{{title}}</div>
<div v-if="rightSide" class="cts-split-grid">
<slot></slot>
</div>
</div>
</template>
and add right Side content as follows :
<AltHeader :title="'POD' + currentPodId" rightSide >
<!-- side right content here -->
</AltHeader>
I have a parent component and multiple child components, which use the same prop. This prop is an array of keys for a dropdown menu in element.js.
When the children render the first time, they contain no data. However, once the keys from arrive using vuefire the children get the dropdown menu items. However, the element dropdown menu is not rerendered as it should have been.
However using the vue dev tools, I can see that the dropdown menu entries have been passed down as a key. When vue does a hot reload, because of a file change, the keys will load.
Once the entries are loaded, I can select the entry and everything works as expected.
I also had the same results using the vuetify dropdown and the HTML dropdown. Both have the same issue.
parent
<template>
<div class="setup">
<h1>Setup</h1>
<div class="selectIngredients" v-for="number in 6">
<setupSelection :bottle="number" :ingredients="options" />
</div>
</div>
</template>
<script>
import {db} from "#/firebaseConfig"
import setupSelection from '#/components/setupSelection';
export default {
components: {
setupSelection,
},
firestore: {
options: db.collection('ingredients'),
},
};
</script>
child
<template>
<div class="ingredientSelector">
<h3>Select for Pump <span>{{bottle}}</span></h3>
<el-select v-model="selected" clearable placeholder="Select" >
<el-option
v-for="ingredient in ingredients"
v-bind:key="ingredient.text"
v-bind:label="ingredient.text"
v-bind:value="ingredient">
</el-option>
</el-select>
<!-- <v-select
v-model="selected"
:items="ingredients"
label="Select a favorite activity or create a new one"
></v-select> -->
<!-- <select v-model="selected" v-for="ingredient in ingredients">
<option :value="ingredient.value">{{ingredient.text}}</option>
</select> -->
</div>
</template>
<script>
import {db} from "#/firebaseConfig";
export default {
props: {
ingredients: { required: true },
bottle: { type: Number, required: true },
},
data() {
return {
selected: ''
}
},
},
};
</script>
I expected the dropdown menu to update once the client received them.
Thank you!
I haven't used Vuefire myself but I read the following in the documentation:
Make sure to create any property added to firestore in data as well
https://github.com/vuejs/vuefire/tree/master/packages/vuefire#firestore-option
Similar advice is given here:
https://vuefire.vuejs.org/vuefire/binding-subscriptions.html#declarative-binding
In your example you don't have options in the parent's data. This would, presumably, leave it non-reactive, leading to the symptoms you describe.
Use a data property for your items, and set them after the options are loaded.
data() {
return {
options: []
}
},
created() {
db.collection('ingredients').then(data=> this.options = data}
}
The promise returned from db.collection('ingredients') is not reactive.
Even better approach would be to set options: null, and show a loading indicator until it is an array.
Explanation of problem
If a user clicks on the login link the view shall jump down to the login window where a user can type in userdata.
I am aware how to do this within a single file using document.getElementById('login-window').scrollIntoView()
However, I have a project with various single Vue.js component files. The login-link is within one "label" component. But the actual login-window is located in another component called "loginWindow", thus also the id / class "login-window" is stored in "loginWindow".
I tried to grab the "login-window" element with getElementById within my "label" component, but I believe it cannot access it since it is in another component.
This is the template code from "loginWindow"
<template>
<LoginGrid class="login-window" :as-grid="true" :class="classes" autocomplete="off">
<OCard class="login-card" :border="false">
<div class="login-headline f-copy f-bold l-color-primary">{{ t('headline') }}</div>
<!-- online state -->
<template v-if="isLogged">
<OButton color="secondary" #click="onClickLogout">{{ t('logout-label') }}</OButton>
</template>
<!-- offline state -->
<template v-else>
<div class="login-inputs">
<LoginInput
class="login-input-card-number" />
...
</div>
...
<OLink
type="secondary"
class="login-mode-link f-regular f-copy-small"
:no-router="true"
#click="onSwitchMode"
>
{{ modeLabel }}
</OLink>
...
</template>
</OCard>
</LoginGrid>
</template>
Here is what I've tried exactly
Within my "label" component I have implemented this method with an else-statement:
export default {
name: 'loginWindow',
...
methods: {
onClick() {
if (this.isLogged) {
...
} else {
if (isBrowser) {
document.getElementById("login-window").scrollIntoView();
}
}
},
},
}
So if the user is not logged-in, then onClick() it should scroll to the id of "login-window".
However, this solution does not work. I get an error saying "Cannot read property 'scrollIntoView' of null".
Any ideas how to do this with JavaScript within a Vue.js component?
login-window is Class not ID in your HTML. Try this:
document.querySelector(".login-window").scrollIntoView();
I'm trying to create a dropdown type component that can have some markup for the title, and then upon hovering reveal more markup. Something like this:
{{#dropdown-menu}}
{{#dropdown-header}}
<span>My Custom Title markup</span>
{{/dropdown-header}}
{{#dropdown-body}}
list of menu items
{{/dropdown-body}}
{{/dropdown-menu}}
The body should only show while some property like isExpanded is true. But if the body is clicked, isExpanded would become false.
I can make a working component that accepts a title property (string), but I can't figure out how to make the title include custom markup.
You can put the yield in your component in an if block. see this jsbin
component:
App.TestShowComponent = Ember.Component.extend({
layoutName: "components/test-show",
expanded: false,
actions: {
toggle: function () {
this.set('expanded', !this.get('expanded'));
}
}
});
index template:
{{#test-show}}
inner stuff
{{/test-show}}
component template:
<button {{action 'toggle'}}>toggle</button>
{{#if expanded}}
{{yield}}
{{/if}}