Slot: access scope variables - javascript

As a simplified example suppose you have this in a component called "my-buttons":
<button v-for='item in items' v-on:click="this.$emit('activate', item)">
<slot>{{ item.name }}</slot>
</button>
If I use the component somewhere else, is there any way to override the slot and access the item.name value? For example:
<my-component items="myItems">
<span class="myspecialstuff">{{ item.name }}</span>
</my-component>
Obviously as it stands now, vue will complain that it can not find item in the scope.

UPDATE2:
the following code does not work due to https://github.com/vuejs/vue/issues/2511. I can't think of anything else. sorry.
UPDATE:
To achieve specific slot overriding, define slots as
<button v-for='item in items' v-on:click="this.$emit('activate', item)">
<slot v-bind:name="'item-'+item.name">{{ item.name }}</slot>
</button>
and override as
<my-component items="myItems">
//you have access to items so you can do this
//specialItems is an array of items which are to be overriden
<span class="myspecialstuff" v-for="item in specialItems" v-bind:slot="'item-'+item.name" >
{{ item.name }}
</span>
</my-component>
Original answer:
that will be very convoluted imho.
there are 2 ways to achieve what you want depending on where <my-component is getting items.
items is passed to my-component as props. (most common case). In that case, you already have access to items and do not need to jump through hoops for it.
<my-component> is getting items through some external sources like a form or api.(less likely) in this case, you can either raise events to pass data or use 2-way bindings through .sync

Related

Vue Named Slots Caveat?

When using Named Slots with Vue (utilizing the older, more verbose API for component slots), if I have a reusable component defined with a template like this:
<template>
<div v-for="field in formFields">
<slot name="`${field.key}_PREPEND`">
<span hidden></span>
</slot>
<slot name="`${field.key}_FIELD`">
<slot name="`${field.key}_LABEL`">{{ field.label }}</slot>
<slot name="`${field.key}_CONTROL`">
<input v-if="field.type === 'text'" v-model="model[field.key]"></input>
<input type="checkbox" v-else-if="field.type === 'checkbox'" v-model="model[field.key]"></input>
</slot>
</slot>
<slot name="`${field.key}_APPEND`">
<span hidden></span>
</slot>
</div>
</template>
(this is essentially a hollowed out version of an auto-form generating component I have)
I can then reuse this component like so:
<auto-form
:fields="someArray"
:model="someObject"
>
<template slot="Name_PREPEND"> This goes before the name field </template>
<template slot="Name_FIELD"> For some reason this isn't being rendered, the default slot markup is</template>
<template slot="Name_APPEND"> This goes after the name field </template>
</auto-form>
For some reason, using the above markup (<auto-form>), the slot "${field.key}_FIELD" is ignored.
If I change the inner markup of the _PREPEND field like so
<slot name="`${field.key}_PREPEND`">
<span hidden>
<slot name="`${field.key}_CONTENT`"></slot>
</span>
</slot>
I similarly cannot override the _PREPEND slot (but can override _CONTENT)
Is this simply a limitation of Vue component slots? i.e. Are nested component slots not allowed?
In this particular case, the limitation would prevent a developer using this AutoForm component from say, overriding both the control and label at once via the _FIELD slot (for my uses I wanted to add logic that made a particular field conditional based on the value of other fields in the form)
In case anyone else runs into this issue, it's a bit of a sneaky one.
It looks like if you do conditional rendering on markup that is supposed to override a slot, the default slot will render in its place when it is not conditionally rendered.
So, the simple solution is to use v-show instead of v-if when you try to override the component slot.
(Has nothing to do with nested component slots as originally suspected)

Vue add one component before a component inside v-for loop dynamically

I have a question on how to add an unread component before a dynamic component (message, can be of a different type, which requires different rendering) inside a v-for loop.
Here is my sample code without the unread panel.
<template>
<div>
<Message v-for="(message, index) in messages" :key="index" :message="message></Message>
</div>
</template>
AIM:
I want to add an unread panel before the message that is unread. And clearly, it will be added once in the template. So I would like another developer when viewing the template, can easily know that it does the job.
How to add Unread Panel
Method 1:
Add it in template directly.
<template>
<div>
<template v-for="(message, index) in messages" :key="index">
<Unread v-if="message.getId() === firstUnreadId"></Unread>
<Message :message="message"></Message>
</template>
</div>
</template>
Problem:
There are lots of unnecessary checking to see if unread is needed to place to the DOM, not to mention, it is difficult for the developer to know that the unread component will only be loaded once.
Method 2:
Put unread component inside messages, and add one more message into messages, so that the v-for loop will mark it work.
<template>
<div>
<template v-for="(message, index) in messages" :key="index">
<Message :message="message"></Message>
</template>
</div>
</template>
<template>
<message :is="currentComponent"></message>
</template>
<script>
export default {
props: {
message
},
computed: {
currentComponent: function() {
switch (this.message.getType()) {
case "unread":
return "Unread";
// ...
}
}
}
}
</script>
Problem:
Unread is definitely not a message, it seems a trick to make it work well.
I don't think any of the methods suggested above is a good one, does anyone have any solution for this? Thanks in advance.
Method 1 looks fine to me. Method 2, as you say, might be tricky to understand later. Otherwise, you could set a firstUnread property directly on the appropriate message in the array, then test that. You could do this when you load the array, or you could make the array a computed property, so that firstUnread is recalculated whenever the array is changed. Does that help?

How to insert a new tag between every two v-for produced tags?

Is there a way I can insert new tag between every two v-for produced tags?
Like Array.join().
Background:
I want to insert a <span>,</span> between every two <router-link></router-link>.
<router-link></router-link> is produced by v-for, the code like this:
<router-link tag="a"
v-for="text in ['vue', 'react', 'JQuery']"
:key="text"
>
{{ text }}
</router-link>
Run it and seems like this:
vue react JQuery
I don't know how to directly insert <span>,</span> between these so I move it into a <div>:
<div v-for="text in ['vue', 'react', 'JQuery']"
:key="text">
<router-link tag="a">
{{ text }}
</router-link>
<span>, </span>
</div>
Run it and seems like this:
vue, react, JQuery,
so the question is that the last ‘,’ is redundant.
I can fix it by v-show:
<div v-for="(text, index) in ['vue', 'react', 'JQuery']"
:key="text">
<router-link tag="a">
{{ text }}
</router-link>
<span v-show="index !== 2">, </span>
</div>
It works nice, but I want to know is there a easy way to do this?
Thanks response.
Your last approach is the way to go, but you should probably use Array.length for processing the last occurrence, in case the array changes.
<span v-show="index !== (items.length - 1)">, </span>
Or use plain CSS (notice I'm adding class route on the div element):
div.route:not(:last-child) > a::after {
content: ',';
text-decoration: none;
}
Another addition to what has already been said. Inverting the order makes it easier to avoid the unneded comma (you always skip the first one, instead of need to take care of the array length)
<div v-for="(text, index) in ['vue', 'react', 'JQuery']"
:key="text">
<span v-if="index !== 0">, </span>
<router-link tag="a">
{{ text }}
</router-link>
</div>
As an addition to what jom said: I'd rather use v-if than v-show, because as the docs say:
prefer v-show if you need to toggle something very often, and prefer v-if if the condition is unlikely to change at runtime.
Since you do not want to toggle the comma at any point, just don't render it at all :)

Angular: Cloning ng-content elements and it's functionality

I'm trying to clone the ng-content items of a component along with any functionality added on the HTML of that content. For example, the markup using the component might look like this:
<custom-component>
<button (click)="doAThing();">A button</button>
</custom-component>
Then I set up my template for custom-component like so:
<ng-template #content>
<ng-conent></ng-content>
</ng-template>
<ng-template *ngTemplateOutlet="content"></ng-template>
<div class="second-area>
<ng-template *ngTemplateOutlet="content"></ng-template>
</div>
My expectation would be that the ng-content would get duplicated into both ngTemplateOutlet areas. What happens is that it pushes to the last outlet and ignores the first. This markup will duplicate normal markup just fine, but ng-content only move to one outlet.
Is this not possible with this technique, am I missing something obvious, or it there another way to clone the contents of ng-content along with any events attached to it?
I found this solution that worked for me. First the HTML, you'll need a directive to wrap the content in so you can reference it. You'll need to use asterisk with directive so it can be duplicated.
<custom-component>
<ng-container *customDirective>
<button (click)="doAThing();">A button</button>
</ng-container>
</custom-component>
The directive doesn't require any extra code. We just need it for a reference.
In your custom-component, you'll need to create a reference to the diretive via #ContentChild like so:
#ContentChild(CustomDirective, { read: TemplateRef }) transcludeTemplate;
Then we use the following for our custom-component HTML avoiding using ng-content tags all together:
<ng-template *ngTemplateOutlet="transcludeTemplate"></ng-template>
<div class="second-area>
<ng-template *ngTemplateOutlet="transcludeTemplate"></ng-template>
</div>
So this isn't really the same as duplicating <ng-content>, but it gives us a similar function. Apparently ng-content not multiplying is intended behavior. So this might be the best way to achieve a similar goal.

I want Handlebar {{#if}} logic inside of a Ember.Handlebars.helper

I am converting someone else's code to Handlebars.js and I'm stuck on converting this tag to its {{#handle-bar}}{{/handle-bar}} counterpart.
The previous coder used an {{#ifCond}} to toggle what 'selected'. This is my component.
{{#dropdown-item }}
{{unbound this.itemName}}
{{/dropdown-item}}
Here is the div i want converted to my component
<div class="dropdownItem" {{bind-attr value=formField_DropdownItemID}}{{#ifCond formField_DropdownItemID value}} selected{{/ifCond}} >
{{unbound this.itemName}}
</div>
My first thought was to just pop the div's logic into the the component, like the next example, but this gave me an error.
{{#dropdown-item bind-attr value=formField_DropdownItemID {{#ifCond formField_DropdownItemID value}} selected{{/ifCond}} }}
{{unbound this.itemName}}
{{/dropdown-item}}
Any suggestions?
You can set those properties to compute. The syntax would be:
{{#dropdown-item selected=computedProperty value=formField_DropdownItemID}}
computedProperty can deal with your conditional logic. The whole idea is to pull that out of handlebars anyways. :)

Categories

Resources