Angular 6 - Use component props as same component attribute value - javascript

I have a component that is passing props down to a child component. I would like to take the resulting value of [info] and use it as my condition for [value]
Basically, is the [info] prop equal to the String good-info ? If yes, set value to Happy otherwise set it to Sad
<some-component [info]="row.calculation.anotherCalculation.information"
[value]="info === 'good-info' ? 'Happy' : 'Sad' "
></some-component >
Of course I could use the same calculation for value that I'm using for info but that seems redundant. Also the calculation used in the info prop is much longer than the example one shown above.

You can refer to the child component with a template reference variable (e.g. #child), which will allow you to get the value of the info property. However, the code below causes an ExpressionChangedAfterItHasBeenCheckedError because one property depends on the other being set in the same detection cycle.
<some-component #child
[info]="row.calculation.anotherCalculation.information"
[value]="child.info === 'good-info' ? 'Happy' : 'Sad' ">
</some-component >
See this stackblitz for a demo.
The exception mentioned above can be avoided if you set value when a change event is triggered from the child component after info has been set:
export class SomeComponent {
private _info: string;
#Output() infoChanged = new EventEmitter<string>();
#Input() public get info(): string {
return this._info;
}
public set info(x: string) {
this._info = x;
this.infoChanged.emit(x);
}
#Input() public value: string;
}
<some-component #child
[info]="row.calculation.anotherCalculation.information"
(infoChanged)="child.value = $event === 'good-info' ? 'Happy' : 'Sad'" >
</some-component>
See this stackblitz for a demo.

I would suggest using a method doing your calculations (in this example only returning the nested attribute), something like
class SomeComponent {
calculate(row: any) {
return row.calculation.anotherCalculation.information;
}
}
Then, in your HTML you can do
<div *ngFor="let row of rows">
<some-component [info]="calculate(row)"
[value]="calculate(row) === 'good-info' ? 'Happy' : 'Sad' "
></some-component>
</div>

Related

How to pass data to child component's #Input from Observable

I have an angular component that I use as a tab in a for loop on the html page:
...
<ng-container *ngFor="let tabData of data$ | async;">
<tab-component
id="{{ tabData.id }}"
name="{{ tabData.name }}"
>
</tab-component>
</ng-container>
<child-component [selectedData]="selectedData"></child-component>
And in the .ts file:
public data$: Observable<Data[]>
public selectedData: Data
ngOnInit() {
this.data$ = this.service.getAllData();
}
ngAfterContentInit() {
this.data$.subscribe(items => this.selectedData = items[0])
}
I would like the first tab to always be the selectedData by default when first loading the page (element 0 in the array).
Then on click or the right/left arrow keys, dynamically update the value of selectedData passed to the child component.
So far, I've tried everything and the value of selectedData in the child component has always been undefined
Please help me, how can I achieve this!
I managed to get it so that the passed value on the child side is no longer undefined with an ngIf, so:
<child-component *ngIf=selectedData [selectedData]="selectedData"></child-component>
Subscribe for allData in the ngOnInIt itself and do check the value of items before assigning it - whether you are getting it or not and if you are not able to find the value there, then there must be the issue with the getAllDataService.
For child component, use double quotes to pass the value like this : <child-component [selectedTab]="selectedTab"></child-component>
Create a dummy variable in the parent ( or a hardcoded value ) and pass it to child. If your child component is working fine, then there's issue with only data and how you are assigning it.
Hope this helps!
Where exactly are you using the selectedData in your template HTML file?
In the snippet you provided there is a selectedTab used, but no selectedData anywhere...
<ng-container *ngFor="let tabData of data$ | async;">
<tab-component
id="{{ tabData.id }}"
name="{{ tabData.name }}"
>
</tab-component>
</ng-container>
<child-component [selectedTab]=selectedTab></child-component>
Also, you can follow #Eugene's advice and do:
ngOnInit() {
this.data$ = this.service.getAllData().pipe(
tap((items) => this.selectedData = items[0])
);
}
without using ngAfterContentInit() and the need to subscribe a second time.
You could use a subject to express the currently selected tab data, then use combineLatest to create an observable of both sources.
private data$: Observable<Data[]> = this.service.getAllData();
private selectedData$ = new BehaviorSubject<Data>(undefined);
vm$ = combineLatest([this.data$, this.selectedData$]).pipe(
map(([tabData, selected]) => ({
tabData,
selectedTab: selected ?? tabData[0]
})
);
setSelected(data: Data) {
this.selectedData$.next(data);
}
Here we create a single observable that the view can use (a view model) using combineLatest. This observable will emit whenever either of its sources emit.
We set the selectedData$ BehaviorSubject to emit an initial value of undefined. Then, inside the map, we set the selectedTab property to use tabData[0] when selected is not yet set. So, initially, it will use tabData[0], but after setSelected() gets called, it will use that value.
<ng-container *ngIf="let vm$ | async as vm">
<tab-component *ngFor="let tabData of vm.tabData"
[id] = "tabData.id"
[name] = "tabData.name"
(click) = "setSelected(tabData)">
</tab-component>
<child-component [selectedTab]="vm.selectedTab"></child-component>
</ng-container>

Conditionally set v-model in Vue

I have a series of inputs that could be either checkboxes or radio buttons, depending on a value in the data of my vue component.
In particular, I have a Question component, and questions may accept only one answer or multiple answers. I have a selected_answers array in my data, and I was thinking I could have the checkboxes target it as their v-model, while the radio buttons could target selected_answers[0]. This way, I don't have to copy-paste che input elements and just change their type and v-model.
So, my solution would look something like this:
<input
:type="question.accepts_multiple answers ? 'checkbox' : 'radio'"
:id="'ans-' + answer.id"
:value="answer.id"
v-model="question.accepts_multiple_answers ? selected_answers : selected_answers[0]"
/>
However, eslint complains about my code:
'v-model' directives require the attribute value which is valid as LHS
What's a way I can accomplish what I'm trying to do?
You cannot use any advanced code inside of v-model (just a basic string), you could export question.accepts_multiple_answers ? selected_answers : selected_answers[0] to a computed and plug the computed to the v-model.
If you need to have a setter, you will need to write a computed setter, this looks like this
computed: {
fullName: {
// getter
get() {
return this.firstName + ' ' + this.lastName
},
// setter
set(newValue) {
var names = newValue.split(' ')
this.firstName = names[0]
this.lastName = names[names.length - 1]
}
}
}
Meanwhile, since v-model is just some sugar syntax, you could also replace it with usual :value + #input (depending of the type of the field). I do prefer to use those 2 than v-model nowadays, especially for the kind of limitations that you do have right now.
You can't really do it like that.
I recommend you to set up a variable when loading your component like that:
data()
{
return {
model_string: '',
}
}
Then you give your variable some values depending on your own conditions
created() {
if (your.first.condition) {
this.model_string = your.value;
} else if (your.other.condition) {
this.model_string = your.value;
}
}
After this, you can use it in your view as you wish
<input v-model="model_string" ..... // your attributes>
Ended up figuring it out by myself.
<input
:type="question.accepts_multiple_answers ? 'checkbox' : 'radio'"
:id="'ans-' + answer.id"
:value="question.accepts_multiple_answers ? answer.id : [answer.id]"
v-model="selected_answers"
/>

use #Attribute in custom attribute directive and data -binding attribute directive

I'm new to Angular, just a question on use #Attribute in attribute directives, below is some code from a book:
#Directive({
selector: "[pa-attr]",
})
export class PaAttrDirective {
constructor(element: ElementRef, #Attribute("pa-attr") bgClass: string) {
element.nativeElement.classList.add(bgClass || "bg-success", "text-white");
}
}
and the template.html:
...
<td pa-attr="bg-warning">{{item.category}}</td>
...
so we can see that use #Attribute we can get the value of the attribute, but if we use data-binding attribute directive as:
<td [pa-attr]="item.category == 'Soccer' ? 'bg-info' : null">...
then the book modify the code as:
export class PaAttrDirective {
constructor(private element: ElementRef) {}
#Input("pa-attr")
bgClass: string;
ngOnInit() {
this.element.nativeElement.classList.add(this.bgClass || "bg-success", "text-white");
}
}
I'm a little bit confused here, can't we use #Attribute to get the value again as:
export class PaAttrDirective {
constructor(element: ElementRef, #Attribute("pa-attr") bgClass: string) {
element.nativeElement.classList.add(bgClass || "bg-success", "text-white");
}
}
why when use the attribute directive with data-binding then we have to create input property in the code and not able to use #Attribute?
They use #Input instead of #Attribute because:
Attributes initialize DOM properties and then they are done. Property
values can change; attribute values can't.
item.category == 'Soccer' ? 'bg-info' : null expression changes attribute value, so your Directive wouldn't get the updated value after it was changed.
I suggest to read about Angular template syntax here.
#Attribute: accepts plain primitive types, eg strings and numbers
#Input: accepts anything/an object, eg your own class object
You pass the string abc to the attribute like this:
<td pa-attr="abc"></td>
You pass the same thing to the input like this:
<td [pa-attr]="'abc'"></td> <!-- note the single quotes -->
Or
in ts
x = 'abc';
in html
<td [pa-attr]="x"></td>
I am not sure if you can have a dash in the input property name.

Angular: Template rerenders Array while its length does not change

Here is a parent component's template:
<ng-container *ngFor="let set of timeSet; index as i">
<time-shift-input *ngIf="enabled"
[ngClass]="{
'mini-times' : miniTimes,
'field field-last': !miniTimes,
'field-central': !canAddSet,
'field-central--long': (canAddSet || canDeleteSet) && !miniTimes }"
[startHour]="set.startHour"
[endHour]="set.endHour"
[endsNextDay]="set.endsNextDay"
[canAddSet]="canAddSet()"
[canDeleteSet]="canDeleteSet(i)"
[required]="true"
(onAddSet)="onAddSet(i)"
(onDeleteSet)="onDeleteSet(i)"
(onChange)="onShiftTimes($event, i)"></time-shift-input>
</ng-container>
Here is the code which will update the timeSet array after onChange event has been triggered:
public onShiftTimes( set: TimeSchedule | Array<TimeSchedule>, ind?: number ): void {
if ( ind !== undefined ) {
this.timeSet[ind] = <TimeSchedule>set;
} else {
this.timeSet = <Array<TimeSchedule>>set;
}
this.timeChanged.emit({
data: this.timeSet,
di: this.dayIndex
});
}
The child component, <time-shift-input> is getting re-rendered every time the onShiftTimes method has been called, EVEN when the the length of the array stays the same.
Which is a bummer, because it breaks user experience in an annoying way (removes focus, etc). I thought that pushing OR updating an index of an existing array won't change the object reference for the array, so the ngFor loop will not be triggered. However ngOnInit in <time-shift-input> is getting called every time after onShiftTimes...
Any ideas how to prevent re-rendering?
RTFM, as they say.
trackByFn to the rescue - that was the simple and correct solution to my problem. More on this gem:
https://angular.io/api/common/NgForOf#ngForTrackBy
https://www.concretepage.com/angular-2/angular-4-ngfor-example#trackBy

How can I create a dynamically interpolated string in javascript?

I'm working on creating a reusable UI component and am trying to figure out how to allow the consumer of the component to provide their own template for a particular area of the component.
I'm using typescript and am trying to utilize string interpolation to accomplish this as it seemed the most appropriate course of action.
Here is what I have so far:
export class Pager {
pageNumber: number = 1;
getButtonHtml(buttonContentTemplate?: string, isDisabled?: boolean): string {
buttonContentTemlpate = buttonContentTemplate || '${this.pageNumber}';
isDisabled = isDisabled || false;
return `<button id="button-id" type="button" ${!isDisabled ? '' : disabledAttribute}>
${buttonContentTemplate}
</button>`;
}
}
I have some other methods that will update the page number based off user input/interaction, but I want it to work that when getButtonHtml gets called, the return value would be <button id="button-id" type="button">1</button>, but instead I'm getting <button id="button-id" type="button">${this.pageNumber}</button>.
Is there a way to get javascript to evaluate the string again, and interpolate the remaining place holders?
I've looked at the MDN article on this topic and think that the String.raw method might possibly be what I need to use, but I wasn't sure and no matter what I try, I haven't gotten it to work.
Any help would be greatly appreciated.
The problem is that Template literals are interpreted immediately.
What you want to do is lazy load the template. So it would be best to pass in a function that returns a string.
export class Pager {
pageNumber: number = 1;
getButtonHtml(template?: () => string, isDisabled=false): string {
template = template || function() { return this.pageNumber.toString() };
return `<button id="button-id" type="button" ${!isDisabled ? '' : disabledAttribute}>
${template()}
</button>`;
}
}
Additionally, you can take advantage of default parameters to avoid the || trick.

Categories

Resources