Angular2 Get component reference dynamically - javascript

I have multiple <mat-button-toggle> elements generated in my app and I want always only one selected. The problem that I now have is, how to get the component reference to the last selected toggle-button when another toggle button is clicked.
I really searched quite a while but couldn't understand how to do it.
component.html
<mat-button-toggle (click)="onKeywordSelect($event)" *ngFor="let keyword of keywords" [id]="keyword.id" [attr.id]="keyword.id" [value]="keyword.id" class="keyword">
<div class="text">{{ keyword.name }}</div>
</mat-button-toggle>
component.ts
// imports and #Component
export class NavbarComponent implements OnInit {
keywords = [new Keyword('name1'), new Keyword('name2')]; // sample data
$selectedKeyword: $ | any; // I've imported JQuery
onKeywordSelect(event: any) {
// This element depends on where you mouse was positioned when clicking
// Most often not the <mat-button-toggle> but some child element
const target = event.target;
// To get to the <mat-button-toggle> component that was clicked
const matButton = $(target).closest('mat-button-toggle');
if (this.$selectedKeyword !== undefined) {
// At the start there is no selected keyword
// TODO: Set the 'checked' property of the cur selected keyword to false
}
this.$selectedKeyword = $matButton;
}
}
I tried it with #ViewChild() but because the id of the selected keyword changes when the user selects one I don't know how to keep track of the selected component reference.
Edit
Forgot to mention: Yes I'm aware of mat-button-toggle-group but I don't want to use it because of some styling. Is there no other way to solve this?

Edit: Updated my ans as your requirement is not to use mat-button-toggle-group:
You can use checked property and set current and last selected value on change event like this:
component.html:
<mat-button-toggle
*ngFor="let keyword of keywords"
value="{{keyword.id}}"
[id]="keyword.id"
[attr.id]="keyword.id"
(change)="this.onChange(keyword.id)"
[checked]="this.currValue === keyword.id">
{{keyword.name}}
</mat-button-toggle>
<div class="example-selected-value">
Last Selected value: {{this.lastValue}}
</div>
<div class="example-selected-value">
Current Selected value: {{this.currValue}}
</div>
component.ts:
keywords: any = [
{id: 1, name: "name1"},
{id: 2, name: "name2"},
{id: 3, name: "name3"},
]
lastValue: string = "";
currValue: string = "";
onChange = (value) => {
this.lastValue = this.currValue;
this.currValue = value;
}
Check Demo Here.

Add mat-button-toggle-group to select only one button and get value of group to get last selected button
see: https://stackblitz.com/angular/yrqebgjbxao?file=app%2Fbutton-toggle-exclusive-example.ts

Related

display text based on the value returned by a class binded to a method

I am running a loop with each item that has a **button **that has a **class **that is **binded **to a method. i want to display a certain text for this button, depending on the value returned by the aforementioned method
HTML Template
<button v-for="(item, index) in items"
:key="index"
:class="isComplete(item.status)"
> {{ text_to_render_based_on_isComplete_result }}
</button>
Method
methods: {
isComplete(status) {
let className
// there is another set of code logic here to determine the value of className. below code is just simplified for this question
className = logic ? "btn-complete" : "btn-progress"
return className
}
}
what im hoping to achieve is that if the value of the binded class is equal to "btn-completed", the button text that will be displayed is "DONE". "ON-GOING" if the value is "btn-in-progress"
my first attempt was that i tried to access the button for every iteration by using event.target. this only returned undefined
another option is to make another method that will select all of the generated buttons, get the class and change the textContent based on the class.
newMethod() {
const completed = document.getElementsByClassName('btn-done')
const progress= document.getElementsByClassName('btn-progress')
Array.from(completed).forEach( item => {
item.textContent = "DONE"
})
Array.from(progress).forEach( item => {
item.textContent = "PROGRESS"
})
}
but this may open another set of issues such as this new method completing before isComplete()
i have solved this by returning an array from the isComplete method, and accessed the value by using the index.
<template>
<button v-for="(item, index) in items"
:key="index"
:class="isComplete(item.status)[0]"
:v-html="isComplete(item.status)[1]"
>
</button>
</template>
<script>
export default {
methods: {
isComplete(status) {
let className, buttonText
// there is another set of code logic here to determine the value of className. below code is just simplified for this question
if (className == code logic) {
className = "btn-complete"
buttonText = "DONE"
}
else if (className != code logic) {
className = "btn-progress"
buttonText = "ON-GOING"
}
return [ className, buttonText ]
}
}
}
</script>

Vue jest test current target parent has class when click (which is added via v-for)

I'm trying to test a selected item on a list of items, which is handled on a click event by finding a selected class added to it.
My template:
<div class="mycomp" v-for="(list, i) in listItem" :key="list.id" :class="{ selected: i === selectedlist}">
<button class="md-primary md-raised selectOption" v-if="i !== selectedList" #click="selectItem(list, i)">Select</button>
</div>
Test case:
test('highlight the selected item', () => {
const mountFunction = options => {
return mount(FlightDetails, {
localVue,
...options
})
}
const wrapper = mountFunction()
wrapper.findAll('.selectOption').at(0).trigger('click')
const flightCard = wrapper.findAll('.office-flightDetails').at(0).classes()
expect(flightCard).toContain('selected')
})
Here, I'm triggering a click event for the first button in the list, and expecting class to be added for the first wrapper of the list. But it is not finding the class:
expect(received).toContain(expected) // indexOf
Expected value: "selected"
Received array: ["listItems"]
In jQuery or JavaScript, I can find the index using eq. Here, I used at is it correct?
I'm inferring the button-click is supposed to cause the selected class to be applied to an element in the .office-flightDetails group (not shown in question).
That class won't be applied until the next rendering tick, so you have to await the trigger('click') call:
test('highlight the selected item', async () => {
//... 👆
👇
await wrapper.findAll('.selectOption').at(0).trigger('click')
})

I want to replace text in component "x" by clicking a button from component "y"

I got two components that i want to connect. In component "x" i got some text in the template file and in component "y" i got a button that i want to click to replace/change the text in component "x".
this is "x" text i want to change:
<p> Text i want to replace </p>
this is "y" component.ts text i want to replace with:
changeTheText: string = "Text i want to change to";
showMyContainer2: boolean = false;
clearMe(){
this.showMyContainer2 = true;
this.UIS.clearStorage();
}
this is "y" component.template:
<button id="clearBtn" (click)="clearMe()">Change the text button</button>
<div *ngIf="showMyContainer2">
{{changeTheText}}
</div>
You can do this by using EventEmitters
https://angular.io/api/core/EventEmitter
Is x a direct child of y? Meaning is the HTML like this?
<y>
<x></x>
</y>
If so, you can use #Input() properties
In x component.ts, do this:
// import Output and EventEmitter from '#angular/core`;
#Input text: string;
And I assume the HTML is:
<p>{{ text }}</p>
Then in y.component.ts, do:
clearMe(){
this.showMyContainer2 = true;
this.UIS.clearStorage();
this.changeTheText = 'New Text you want'
}
And in y.html, do:
<div class="y">
<x [text]="changeTheText"></x>
</div>
You can possibly use EventEmitters like Mamosek mentioned but it depends on the heirarchy of x and y. Are they parent => child or child => parent.
If they don't have parent child relationship, you have to create a middle man Service that has a BehaviorSubject and both x and y have to inject this Service and communicate through that BehaviorSubject by doing .next to push a new value and .subscribe to listen to values.
====================== Edit ================================
Does it make sense for the text to live in component Z?
Component Z.ts
textToChange = 'Change this text';
changeTheText() {
this.textToChange = 'new text';
}
Component Z.html
<div class="z">
// feed the text down to the x component
<x [text]="textToChange"></x>
// listen to the textChanged event from component Y and every time it happens call the function changeTheText
<y (textChanged)="changeTheText()"></y>
</div>
Component X.ts
// take input of text from parent's location
#Input() text: string;
Component X.html
<div>{{ text }}</div>
Component Y.ts
#Output textChanged: EventEmitter<void> = new EventEmitter();
changeTheText() {
// emit the event so the text can change in component Z
this.textChanged.emit();
}
Component Y.html
<button (click)="changeTheText()">Change the text</button>
I freestyled all of that so there might be some errors but you get the idea.
If the text cannot live in component Z, you will have to have a centralized approach like I mentioned before this edit.
import { Injectable } from '#angular/core';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
#Injectable({
providedIn: 'root',
})
export class TextChangeService {
text = new BehaviorSubject('initial value of text');
constructor() { }
}
component X.ts (The HTML's for both components remain the same)
text: string;
constructor(textService: TextService){ }
ngOnInit() {
this.text.subscribe(text => this.text = text);
}
component Y.ts
constructor(textService: TextService){ }
changeTheText() {
this.textService.next('New text');
}
This last approach I showed you, I don't recommend because it turns into convoluted code the more you build on it. I suggest you learn the Redux pattern and use NGRX.

When is the change reflected on Model in Angular2 select

I have a select field like this:
<div *ngFor="let filter of filters; let idx = index">
<select [id]="'name' + idx" [(ngModel)]="filter.name" (change)="changeFilter(idx, $event)">
<option val="a">A</option>
<option val="b">B</option>
</select>
</div>
My change() function on the component doesn't detect the change instantly. Simplified:
#Component()
export class Filters {
public filters = [{name: "a"}, {name: "b"}, {name: "a"}];
public change(idx: number, $event: Event) {
console.log(this.filters[idx].name === $event.target.name); // false here
setTimeout(() => {
console.log(this.filters[idx].name === $event.target.name); // Now it's true
}, 10);
}
}
Now, if I change between the options, the change() function needs some time - usually less then 3 milliseconds on that setTimeout, but sometimes more.
Now, I am sure this is not the best way to detect the change, and I'll find out how to do it properly, but I'm curious as to how to determine when is the change reflected on my model?
ngModel doesn't support binding to variables created by ngFor.
Use instead
[(ngModel)]="filters[idx].name"
You could also try
(ngModelChange)="changeFilter(idx, $event)"
ngModelChange is probably emitted after the value was changed while for (change) it depends on the browser what event and event handler is processed first (AFAIR ngModel uses input)

In Vue.js, change value of specific attribute for all items in a data array

I'm trying to toggle an open class on a list of items in a v-repeat. I only want one list item (the one most recently clicked) to have the class open.
The data being output has a "class" attribute which is a blank string by default. I'm using this to set the class of the list items in the v-repeat like so:
<li v-repeat="dataSet"
v-on="click: toggleFunction(this)"
class="{{ class }}">
{{ itemContent }}
</li>
I'm using v-on="click: toggleFunction(this)" on each item, which lets me change the class for the specific item, but how do I change the class on all the other items?
My current on-click method:
toggleFunction: function(item) {
if (item.class == '') {
// code to remove the `open` class from all other items should go here.
item.class = 'open';
} else {
item.class = '';
}
}
I've tried using a regular jQuery function to strip the classes: that does remove the classes but it doesn't change the item.class attribute, so things get weird once an item gets clicked more than once...
I'm sure there must be a straightforward way to fix this that I'm not seeing, and having to set a class attribute in the data itself feels hacky anyway (but I'll settle for any fix that works).
I just ran into the same issue. I am still learning Vue, but I tackled it using the "v-class" directive. Using v-class, any time an active value is true for a record, it will automatically add the class "open" to the list element. Hope this JSFiddle helps.
<ul>
<li
v-repeat="people"
v-on="click: toggleActive(this)"
v-class="open: active">{{ name }}
</li>
</ul>
new Vue({
el: '#app',
data: {
people: [
{name: 'mike'},
{name: 'joe',active: true},
{name: 'tom'},
{name: 'mary'}
]
},
methods: {
toggleActive: function(person) {
// remove active from all people
this.people.forEach(function(person){
person.$set('active',false);
});
//set active to clicked person
person.$set('active',true);
}
}
});

Categories