Vue component as child of another component - javascript

I am working on converting an existing theme into reusable components.
I currently have a button component like so:
<template>
<a :href="link" class="button" :class="styling"><slot></slot></a>
</template>
<script>
export default {
props: {
link: {},
styling: {
default: ''
}
}
}
</script>
And, in my HTML, I use it like so:
<vue-button link="#" styling="tiny bg-aqua">Button 1</vue-button>
Now, I am attempting to create a "button group" using the existing button component.
What I would like to be able to do is something like this:
<vue-button-group styling="radius tiny">
<vue-button link="#" styling="tiny bg-aqua">Button 1</vue-button>
<vue-button link="#" styling="tiny bg-aqua">Button 2</vue-button>
<vue-button link="#" styling="tiny bg-aqua">Button 3</vue-button>
<vue-button link="#" styling="tiny bg-aqua">Button 4</vue-button>
</vue-button-group>
I am very new to VueJS, and am a bit confused on the proper way to handle such a thing. I would like to be able to pass as many button components into the group as is needed.
Here is what I have so far for the button group:
<template>
<ul class="button-group" :class="styling">
<li><slot></slot></li>
</ul>
</template>
<script>
export default {
props: {
styling: {
default: ''
}
}
}
</script>
This would, of course, work with a single button being passed in, but I cannot seem to figure out how to allow for more than that, while encasing each button within its own list item.
Any suggestions or corrections to the way I am going about this would be very much appreciated. Thank you.

Since you want to do advanced things with the output of your component, this might be the time to go to render functions, as they allow far more flexibility:
Vue.component('button-group', {
props: {
styling: {
default: ''
}
},
render(createElement) { // createElement is usually called `h`
// You can also do this in 1 line, but that is more complex to explain...
// const children = this.$slots.default.filter(slot => slot.tag).map(slot => createElement('li', {}, [slot]))
const children = [];
for(let i = 0; i < this.$slots.default.length; i++) {
if(!this.$slots.default[i].tag) {
// Filter out "text" nodes, so we don't make li elements
// for the enters between the buttons
continue;
}
children.push(createElement('li', {}, [
this.$slots.default[i]
]));
}
return createElement('ul', {staticClass: "button-group",class: this.styling}, children);
}
})
var app = new Vue({
el: '#app',
})
.rainbow-background {
/* todo: implement rainbow background */
border: red 1px solid;
}
.button-group {
border-color: blue;
}
<script src="https://cdn.jsdelivr.net/npm/vue#2.5.17/dist/vue.js"></script>
<div id="app">
<button-group styling="rainbow-background">
<button>hi</button>
<button>How are you?</button>
<button>I'm fine</button>
<button>Have a nice day!</button>
</button-group>
</div>
A render function works by returning an virtual html structure, this structure is generated by repeated calls to a createElement functions. createElement accepts 3 parameters, a tag name (like ul or li), an options object, and an list of children.
We first start by generating a list of children with our incoming slots, who are stored inside this.$slots.default.
Then we loop over this list, filtering out incoming slot data that is basically text, this is because the way HTML considers whitespace between tags as text.
We are now almost complete with our final structure, we now wrap the slot element in a freshly generated li tag, and then we finish the generating of with wrapping everything in a new ul tag with the proper class names.

The Vue way to do this is use names slots, and provide the data the child uses to the parent. The data will propagate to the child via the slot-scope.
The whole key to this is the data flows through the parent to the child.
Here's a working codepen of the process: https://codepen.io/Flamenco/pen/ZMOdYz
This example uses the default slot, so name attribute is not needed on parent or child.
Parent
<template>
<ul class="button-group" :class="styling">
<li v-for='item in items'><slot :item='item'></slot></li>
</ul>
</template>
<script>
...
props:{
items: Array
}
</script>
Child
<vue-button-group class="radius tiny" :items='items'>
<template slot-scope='scope'>
<vue-button link="#" styling="tiny bg-aqua">{{scope.item.text}}</vue-button>
</template>
</vue-button-group>
<script>
...
data:{
items:[{text:'Button 1'},{text:'Button 2'},{text:'Button 3}]
}
</script>

You can also try to use a named slot
<template>
<ul class="button-group">
<li><slot name="buttons"></slot></li>
</ul>
</template>
And than:
<vue-button-group class="radius tiny">
<template slot="buttons">
<vue-button link="#" styling="tiny bg-aqua">Button 1</vue-button>
<vue-button link="#" styling="tiny bg-aqua">Button 2</vue-button>
<vue-button link="#" styling="tiny bg-aqua">Button 3</vue-button>
<vue-button link="#" styling="tiny bg-aqua">Button 4</vue-button>
</template>
</vue-button-group>

A possible solution for that is to use v-for.
<button v-for="button in buttons" :key="button">
Button{{button}}
</button>
Here is the fiddle.
From there you could build your <buttongroup>-component yourself; passing "meta-infos" into your <buttongroup> as props (in my fiddle the array in the data-part).
Slots would make sense, if you want to render something else besides the buttons and you want to inject components for that. Otherwise you would gain nothing with slots.

Related

accessing *ngFor last in its component in angular 6

In angular 6 I want to access *ngFor last value as I want to operation if last value is set
eg
<li [ngClass]="list.mydata==1?'replies a':'sent a'" *ngFor="let list of chatlist; let last=last;">
<span [last]="last"></span>
<img src="{{list.profile_img}}" alt="" />
<div *ngIf="list.sender_type==0">
<p>{{list.message}}{{last}}</p>
</div>
<div *ngIf="list.sender_type==1">
<p style="background-color: burlywood;">{{list.message}}</p>
</div>
</li>
I want to do is [(myvar)]=last in place of let last=last
I want to bind the last variable so, I can access it is set or not in its component.
you can create a custom directive:
import { Directive, Output, EventEmitter, Input } from '#angular/core';
#Directive({
selector: '[onCreate]'
})
export class OnCreate {
#Output() onCreate: EventEmitter<any> = new EventEmitter<any>();
constructor() {}
ngOnInit() {
this.onCreate.emit('dummy');
}
}
and then you can use it in your *ngFor to call the method in your component:
<li [ngClass]="list.mydata==1?'replies a':'sent a'" *ngFor="let list of chatlist; let last=last;">
<span (onCreate)="onCreate(last)"></span>
<img src="{{list.profile_img}}" alt="" />
<div *ngIf="list.sender_type==0">
<p>{{list.message}}{{last}}</p>
</div>
<div *ngIf="list.sender_type==1">
<p style="background-color: burlywood;">{{list.message}}</p>
</div>
</li>
then in your component:
myvar: boolean = false;
onCreate(last) {
this.myvar = last;
}
checkout this DEMO.
Angular provides certain local variables when using *ngFor, one is for example last, which will (not as you expect currently) be a boolean value, being true if it is the last item. This is meant for adding specific stylings for the example to the last element of a list.
If you want that boolean you already use it correctly, but obviously the element using it should be a component. So instead of
<span [last]="last"></span>
it should be something like
<my-component [last]="last"></my-component>
where in my-component you define
#Input last: boolean;
and thus have access to it.
for example
<li [ngClass]="list.mydata==1?'replies a':'sent a'" *ngFor="let list of chatlist; let index=i;">
</li>
now you excess last elemnt like
<anyTag *ngIf="i === chatlist.length-1"></anyTag>

Run function when element was created

I have some elements created by v-for. How can I run the function only once by keeping 'for every element creation' as a condition .
<div v-for"value in values">
<div # function(value, domElement) if value.bool===true #>
</div>
the easiest way, IMO, would be to make each of those elements a Vue Component & pass the function down as a prop.
File One
<div v-for="value in values">
<Custom-Component :propValue="value" :propFunction="functionYouNeed" />
</div>
Custom Component
<template>
<div> {{propValue.value}} </div>
</template>
<script>
export default {
props: ['propFunction', 'propValue'],
created(){
if (this.propValue.bool === true) {
this.propFunction()
}
}
}
</script>
It's not so clear what exactly you want:
<div # function(value, domElement) if value.bool===true #>
So, here's all possible solutions you want to implement.
You can bind the method using once modifier:
<div #click.once="yourMethod">
Or, if you want not to change the content then you can use v-once:
<div v-once>{{ neverChanged }}</div>
But if you just need to use the function when it was created then call the function inside created property and do not bind the method anywhere else.
created() {
if(condition) {
this.yourMethod()
}
}

How can i change the attribute of a field in Ember JS from a route

In My application i have a template file with 2 fields(say name1 / name2).
Based on one parameter("preference") in the route i want to display either Recipe1 or Recipe2 on the screen.
For eg: if preference is veg, i should display Recipe1 else Recipe2.
i tried this as below but it did not work.
export default Ember.Route.extend({
beforeModel(){
if (preference==='veg'){
console.log("Inside Veg..");
Ember.$("#Recipe2").attr("type","hidden");
}
else {
console.log("Inside Non Veg..");
Ember.$("#Recipe1").attr("type","hidden");
}
What i see is that it goes inside the if/else loop but the ember.$ statements dont make any difference.Please help.
First of all you should not write Ember.$ inside beforeModel hook. that's wrong. When beforeModel hook called, DOM will not be ready. I prefer you to create component and pass preference property to component and have if check to display it in hbs
Create my-receipe component and include it in template.hbs
{{my-receipe preference=preference }}
my-receipe.hbs
{{#if isVeg}}
<input type="text" id="Recipe1" />
{{else}}
<input type="text" id="Recipe2" />
{{/if}}
my-receipe.js
Create isVeg computed property which will return true if the preference is veg.
export default Ember.Component.extend({
isVeg: Ember.computed('preference', function() {
return Ember.isEqual(this.get('preference'), 'veg');
})
})

Vue.JS 2.0 slow when modifying unrelated data

Suppose I have an input field in Vue.JS that v-model bind to a String data property, and a long list of random numbers that are completely unrelated to that first String.
data: {
input: "",
randoms: []
}
<input type="text" v-model="input">
<p v-for="random in randoms" v-text="random"></p>
When I put both in the same Vue, I see a huge slowdown when typing in the input field, as it appears Vue is reevaluating the DOM for each list entry after every input event, although they really have nothing to do with each other.
https://jsfiddle.net/5jf3fmb8/2/
When I however move the v-for to a child component where I bind randoms to a prop, I experience no such slowdown
https://jsfiddle.net/j601cja8/1/
Is there a way I can achieve the performance of the second fiddle without using a child-component?
Is there a way I can achieve the performance of the second fiddle without using a child-component?
Short answer
No.
Long answer
Whenever any dependency of the template changes, Vue has to re-run the render function for the entire component and diff the new virtualDOM against the new one. It can't do this for this or that part of the template only, and skip the rest. Therefore, each time the input value changes, the entire virutalDOM is re-rendered.
Since your v-for is producing quite a bit of elements, this can take a few 100ms, enough to be noticable when you type.
Extracting the heavy part of the template into its own component is in fact the "official" way to optimize that.
As Alex explained, v-model.lazy might improve the situation a bit, but does not fix the core of the issue.
Shortest, simplest answer: change v-model to v-model.lazy.
When I put both in the same Vue, I see a huge slowdown when typing in the input field, as it appears Vue is reevaluating the DOM for each list entry after every input event, although they really have nothing to do with each other.
Note that the OnceFor sample still chugs like mad despite not actually being reactive any more. I don't understand Vue well enough to say if that's intentional or not.
const Example = {
data() { return { input: "", randoms: [] } },
created() { this.newRandoms() },
methods: {
newRandoms() { this.randoms = Array(50000).fill().map(() => Math.random()) }
}
}
new Vue({
el: "#vue-root",
data(){ return {example: 'lazy-model'}},
components: {
LazyModel: {...Example, template: "#lazy-model"
},
OnceFor: {...Example, template: "#once-for"
},
InlineTemplate: {...Example, template: "#inline-template",
components: {
Welp: {
props: ['randoms']
}
}
}
}
})
button,
input,
div {
margin: 2px;
}
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<div id="vue-root">
<span><button v-for="(component, name) in $options.components" #click="$set($data, 'example', name)">{{name}}</button></span>
<component :is="example"></component>
</div>
<template id="lazy-model">
<div>
<input type="text" v-model.lazy="input"><br>
<input type="submit" value="Regenerate" #click="newRandoms">
<p v-for="random of randoms" v-text="random"></p>
</div>
</template>
<template id="once-for">
<div>
<input type="text" v-model="input"><br>
<input type="submit" value="Regenerate" #click="newRandoms">
<p v-for="random of randoms" v-text="random" v-once></p>
</div>
</template>
<template id="inline-template">
<div>
<input type="text" v-model="input"><br>
<input type="submit" value="Regenerate" #click="newRandoms">
<welp :randoms="randoms" inline-template>
<div>
<p v-for="(random, index) of randoms" :key="index"> {{index}}: {{random}} </p>
</div>
</welp>
</div>
</template>

Access parent dom-repeat item on clicking child dom-repeat item

I am using polymer.I have array of objects it looks
[{
name:xxx,
address:yyy,
times:[
{start:12,
End:5
},
{start:2,
End:4
}
]
},
{//same format repeats
}
]
I used nested dom-repeat,
<template is="dom-repeat" items="{{pList}}" as="list">
<paper-item>
<paper-item-body two-line>
<div>[[list.address]]</div>
<div secondary>[[list.name]]</div>
</paper-item-body>
<template is="dom-repeat" items={{list.times}} as="time">
<paper-item-body on-tap="_handleTime" two-line>
<div>[[time.start]]</div>
<div>[[time.end)]]</div>
</paper-item-body>
</template>
</paper-item>
</template>
I have on tap function in second dom-repeat,so on taping below function is called,here I can access time object.
how can I access name and address which is in first dom-repeat using with 'e' reference as bellow?
I tried parentElement but its not working!
_handleTime:function(e) {
console.log(e.model.time); //displays time obj i.e {start:12,End:5} but I'm trying to get {name,address,{start,end}}
console.log(e.parentElement);//gives error
//I'm trying to get entire object like {name:xxx,address:yyy,times:[]}
}
var item = this.$.firstRepeat.itemForElement(e.target);
where firstRepeat is the id of the first dom-repeat,
check this example

Categories

Resources