VueJS conditional class in template - vue.js

I have this template that renders cards of each individual projects. These projects are being pulled from Firebase and stored in an array
<template>
<div class="projects-wrapper">
<div class="individual-project"
v-bind:key="project.id"
v-for="project in projectsCollection"
:title="project.title"
>
<Project :project="project"
v-on:update-likes="updateLikes"
v-on:expand-card="expandCard"
v-on:close-card="closeCard" />
</div>
</div>
</template>
<script>
import Project from './Project'
export default {
name: 'Projects',
data() {
return {
projectsCollection: [],
}
},
I need to implement a functionality of resizing the card (in this example it is "individual-project" div) based on project key's value.
Now, I cannot just use :class="expanded-class, isExpanded" where isExpanded is a key in the project because I need to iterate through array (projectsCollection) and then iterate through an object to get this value.
Hence template does not see this key and fails to apply the class.
How would one approach this task? I am pretty new to Vue, so trying to figure things on the fly.

You don't need to iterate the collection as you have access to each individual project.
You can use the property directly on your div:
<template>
<div class="projects-wrapper">
<div class="individual-project"
v-bind:key="project.id"
v-for="project in projectsCollection"
:title="project.title"
:class="{'expanded-class': project.isExpanded}"
>
<Project :project="project"
v-on:update-likes="updateLikes"
v-on:expand-card="expandCard"
v-on:close-card="closeCard" />
</div>
</div>
</template>

Related

Vue: toggling between two instances of the same component doesn't update the view

I have a setup in a Vue-powered UI, where the user can toggle the contents of a certain div between several options, and two of those options happen to be instances of the same child component (with different properties passed in).
Everything works fine when displaying any given content page for the first time, or when toggling between two unrelated content pages. However when toggling between the two pages which both use the same child component, the div content doesn't get updated.
In code it looks (greatly simplified) like this:
Parent component
<template>
<div>
<!-- toggle buttons -->
<div class="page-button" #click="page=1">About</div>
<div class="page-button" #click="page=2">Dog List</div>
<div class="page-button" #click="page=3">Cat List</div>
<!-- page content -->
<div v-if="page===1">some plaintext here...</div>
<div v-if="page===2">
<childComponent :state="state" listName="dogs" />
</div>
<div v-if="page===3">
<childComponent :state="state" listName="cats" />
</div>
<!-- rest of file omitted -->
childComponent.vue
<template>
<div>
<template v-for="(item, index) in items">
<div>{{ index }}: {{ item.label }}</div>
<!-- etc.. -->
</template>
</div>
</template>
<script>
module.exports = {
props: ['state', 'listName'],
data: function () {
return {
items: this.state.lists[this.listName],
}
},
}
</script>
In the above, state is a global state object that all components have access to, with state.lists.dogs and state.lists.cats being regular arrays.
When the UI initializes with page set to 2 or 3, everything works correctly - the dog list shows for page 2, and the cat list shows for page 3. Likewise, when I click page 2, then page 1, then page 3, everything is fine. However when toggling back and forth between page 2/3, the vue doesn't re-render the child component.
I assume it's possible to work around this by changing the underlying data structure or by binding the child component differently. But is there a straightforward way to make Vue re-render the component as expected?
I guess what you see is Vue trying to optimize rendering by reusing existing component instance. Add key attribute on your childComponent with different values...
<!-- page content -->
<div v-if="page===1">some plaintext here...</div>
<div v-if="page===2">
<childComponent :state="state" listName="dogs" key="dogs" />
</div>
<div v-if="page===3">
<childComponent :state="state" listName="cats" key="cats" />
</div>
<!-- rest of file omitted -->
Other solution (and much better IMHO) is to make your component "reactive" to prop changes - instead of using props to initialize the data() (which is "one time" thing - data() is executed only once when component is created), use computed
module.exports = {
props: ['state', 'listName'],
computed: {
items() {
return this.state.lists[this.listName]
}
},
}
You can use v-show if you just want to render it before hand. Its more costly but it should work without any issues.
<template>
<div>
<!-- toggle buttons -->
<div class="page-button" #click="page=1">About</div>
<div class="page-button" #click="page=2">Dog List</div>
<div class="page-button" #click="page=3">Cat List</div>
<!-- page content -->
<div v-show="page===1">some plaintext here...</div>
<div v-show="page===2">
<childComponent :state="state" listName="dogs" />
</div>
<div v-show="page===3">
<childComponent :state="state" listName="cats" />
</div>
<!-- rest of file omitted -->

Why isn't the value of the object properties inserted here?

I started learning vue yesterday and I'm now fiddling around on the CLI3.
Currently I'm trying out the different approaches to inserting data into my markup.
Here, I basically want to make a "list of Lists".
This here is list1:
<template>
<div>
<ul v-for="item in items">
<li :text="item"></li>
</ul>
</div>
</template>
<script>
export default{
name:"list1",
data() {
return {
items: {
item1 : "itemA",
item2 : "itemB",
item3 : "itemC"
}
}
}
}
</script>
This is the list of lists:
<template>
<div>
<h1>All my stuff in a biiig list!</h1>
<listOfLists />
</div>
</template>
<script>
import listOfLists from '#/components/listOfLists.vue'
export default {
name: 'myComplexView.vue',
components: {
listOfLists
}
}
And this is inserted into myComplexView.vue inside views (im working with routing as well, though it doesnt work perfectly yet as you will see on the screenshot), which you can see here:
<template>
<div>
<h1>All my stuff in a biiig list!</h1>
<listOfLists />
</div>
</template>
<script>
import listOfLists from '#/components/listOfLists.vue'
export default {
name: 'myComplexView.vue',
components: {
listOfLists
}
}
</script>
This is the result Im getting:
https://imgur.com/H8BaR2X
Since routing doesnt work correctly yet, I had to enter the url into the browser manually. Fortunately, the site at least loaded that way as well, so I can tackle these problems bit by bit ^^
As you can see, the data was iterated over correctly by the v-for.
However, the data wasn't inserted in the text attribute of the li elements.
I'm a bit clueless about the cause though.
Maybe I'm not binding to the correct attribute? Vue is using its own naming conventions, based off standard html and jquery as far as I understood.
You've got this in your template:
<li :text="item"></li>
This will bind the text attribute to the value, outputting, e.g.:
<li text="itemA"></li>
You should be able to see this in the developer tools. In the picture you posted you hadn't expanded the relevant elements so the attributes can't be seen.
I assume what you want is to set the content. For that you'd either use v-text:
<li v-text="item"></li>
or more likely:
<li>{{ item }}</li>
Either of these will output:
<li>itemA</li>
On an unrelated note, I would add that this line will create multiple lists:
<ul v-for="item in items">
It's unclear if that's what you want. You're going to create 3 <ul> elements, each with a single <li> child. If you want to create a single <ul> then move the v-for onto the <li>.

Vue.js dynamic layout renderless component layout with multiple slots

I am trying to build a dynamic layout for my application. I have two different layouts, one being DefaultLayout.vue:
<template>
<div>
<main>
<slot/>
</main>
</div>
</template>
and a second one being LayoutWithFooter.vue, with two slots:
<template>
<div>
<main>
<slot/>
</main>
<footer>
<slot name="footer"/>
</footer>
</div>
</template>
My renderless component to handle the dynamic layout looks like this:
<script>
import Vue from 'vue';
import DefaultLayout from './DefaultLayout';
import LayoutWithFooter from './LayoutWithFooter';
export default {
props: {
name: {
type: String,
required: true
}
},
created(){
this.registerComponent("DefaultLayout", DefaultLayout);
this.registerComponent("LayoutWithFooter", LayoutWithFooter);
this.$parent.$emit('update:layout', this.name);
},
methods: {
registerComponent(name, component) {
if(!Vue.options.components[name]) {
Vue.component(name, component);
}
}
},
render() {
return this.$slots.default[0];
},
}
</script>
All of this works fine for the DefaultLayout.vue but when I want to use the LayoutWithFooter.vue, it cannot handle the two slots inside it. Here's an example usage:
<template>
<layout name="LayoutWithFooter">
<div>
<div>some content</div>
<div slot="footer">content for the footer slot</div>
</div>
</layout>
</template>
Problem now is, that the "content for the footer slot" does not get rendered inside of the footer slot of the LayoutWithFooter.vue.
First of all I want you to pay an attention to defining slots level in your example. You provided this code:
<template>
<layout name="LayoutWithFooter">
<div>
<div>some content</div>
<div slot="footer">content for the footer slot</div>
</div>
</layout>
</template>
But actually your div slot="footer" does not refer to footer slot of LayoutWithFooter.vue component. It because of anyway the very first child refers to default slot. And as a result it looks like:
"You want to set content for default slot and inside this default slot you tried to set content for footer slot" - but it's two different scopes.
The right options would look like on the next example:
<template>
<layout name="LayoutWithFooter">
<!-- default slot content -->
<div>
<div>some content</div>
</div>
<!-- footer slot content -->
<div slot="footer">content for the footer slot</div>
</layout>
</template>
I prepared some example based on code and structure you've provided. There you are able to switch layouts and check out how it works and use different components slot in one layout.
Check it here:
https://codesandbox.io/s/sad-fog-zr39m
P.S. Maybe some point are not totally clear, please reply on my answer and I will try to explain more and/or provide you with more links and sources.

Reference dynamically created component in Aurelia

I know I can create a reference to my component in my view model like this:
.html:
<template>
<mdfield view-model.ref="ref"></mdfield>
</template>
.ts:
export class Vm {
ref: any;
test(){
console.log(this.ref);
}
}
This works, but what is the syntax if I'm creating the components dynamically? Like this:
<template>
<div repeat.for="field of fields">
<mdfield view-model.ref="<what goes here?>"></mdfield>
</div>
</template>
I guess I want to add them to an array in my viewmodel for later reference, but how?
$index gives you the current index of the repeat.for. So, if you want to add the view-model references to an array:
<div repeat.for="field of fields">
<mdfield view-model.ref="refArray[$index]"></mdfield>
</div>

Providing the model for a component as a slot

Consider the following two custom elements in Aurelia (list & row):
row.html
<template>
<span>${name}</span>
</template>
row.js
export class Row
{
name = "Marry";
}
list.html
<template>
The List
<ol>
<li repeat.for="r of rows">
<slot name="rowItem" model.bind="r"></slot>
</li>
</ol>
</template>
list.js
import { bindable } from 'aurelia-framework';
export class List
{
#bindable
rows = [{name: "John"}];
}
The app will tie them together:
app.html
<template>
<require from="./list"></require>
<require from="./row"></require>
<list rows.bind="users">
<row slot="rowItem"></row>
</list>
</template>
app.js
export class App
{
users = [{name: "Joe"}, {name: "Jack"}, {name: "Jill"}];
}
The problem is that the model for the row is not set correctly. All I get as the output is the following:
The List
1.
2.
3.
So the question is; how can I provide the model for a slot in Aurelia?
Here's a Gist to demonstrate the problem in action.
Slots aren't going to work for what you want to do. It's a known limitation of slots in Aurelia. Slots can't be dynamically generated (such as inside a repeater).
Luckily, there's another option to accomplish what you want: template parts.
Template parts aren't well documented (my fault, I should have written the docs for them). But we have some docs in our cheat sheet. I've modified your gist to show how to use them: https://gist.run/?id=1c4c93f0d472729490e2934b06e14b50
Basically, you'll have a template element in your custom element's HTML that has the replaceable attribute on it along with a part="something" attribute (where something is replaced with the template part's name. Then, when you use the custom element, you'll have another template element that has the replace-part="something" attribute (again, where something is replaced with the template part's name). It looks like this:
list.html
<template>
The List
<ol>
<li repeat.for="row of rows">
<template replaceable part="row-template">
${row}
</template>
</li>
</ol>
</template>
app.html
<template>
<require from="./list"></require>
<require from="./row"></require>
<list rows.bind="users">
<template replace-part="row-template">
<row name.bind="row.name"></row>
</template>
</list>
</template>