Angular Material how to select a component in JS - javascript

I have an angular webapp using angular material. In my HTML I have two ButtonToggles that you can toggle with a simple click event handler which I handle myself. However, there should also be a way to toggle the buttons with a keyboard shortcut. I have a keypress event listener that correctly intercepts the keypress, but I have no idea how I can toggle the associated button because I can't pass it in to the event handler.
Here is my html
<mat-button-toggle-group [multiple]="schema.multi">
<mat-button-toggle *ngFor="let label of schema.labels"
(click)="toggleAnnotation(label, localButton)"
[value]="label.name"
#localButton
[style.background-color]="localButton.checked == true ? label.backgroundColor : 'ghostwhite'">
{{label.name}} ({{label.shortcut}})
</mat-button-toggle>
</mat-button-toggle-group>
</div>
And the related typescript:
import {Component, EventEmitter, HostListener, Input, OnChanges, Output, SimpleChanges} from '#angular/core';
import {Label} from '../sequence/models/label';
import {TagAssignment} from '../../../models/tag-assignment';
import {MatButtonToggle, MatButtonToggleGroup} from "#angular/material/button-toggle";
export class CategoricalTaggerSchema {
multi: boolean; // can the user select multiple tags at once
prompt: string; // message to display before showing the tagger document
labels: Label[]; // categories to select from
}
#Component({
selector: 'app-categorical',
templateUrl: './categorical-tagger.component.html',
styleUrls: ['./categorical-tagger.component.css']
})
export class CategoricalTaggerComponent implements OnChanges {
#Input() config: TagAssignment = new TagAssignment(); // default to some value
#Output() valueChange = new EventEmitter<string>();
#Output() validChange = new EventEmitter<boolean>();
#Input() disabled = false;
schema: CategoricalTaggerSchema = {multi: false, prompt: '', labels: []};
annotations = new Set<string>(); // much simpler then sequence tagging, just a list of named labels
constructor() {}
ngOnChanges(changes: SimpleChanges): void {
if (changes.hasOwnProperty('config')) {
this.schema = JSON.parse(this.config.tag_schema);
}
}
toggleAnnotation(label: Label, localButton) {
if (!this.disabled) {
if (this.annotations.has(label.name)) {
this.annotations.delete(label.name);
localButton.checked = false;
} else { // creating new annotation
if (!this.schema.multi) { // only one annotation allowed
this.annotations.clear();
}
this.annotations.add(label.name);
}
}
this.emitChanges();
console.log(this.annotations);
}
emitChanges() {
this.valueChange.emit(this.getValue());
this.validChange.emit(this.isValid());
}
#HostListener('document:keypress', ['$event'])
handleKeyboardEvent(event: KeyboardEvent) {
// detect keypress for shortcut
for (const label of this.schema.labels) {
if (label.shortcut === event.key) {
this.toggleAnnotation(label, null);
break;
}
}
}
getValue(): string {
return JSON.stringify(Array.from(this.annotations));
}
isValid(): boolean {
return (this.annotations.size > 0);
}
reset(): void {
this.annotations.clear();
}
}
The only thing I can think of is somehow fire a function from the HTML on component load that adds all the toggle buttons to an array or map which the TS has access to, and search them up by shortcut when I need them, but this seems like a hacky solution.
EDIT: I've tried using ViewChild, but since I can't initialize the ids dynamically (angular viewChild for dynamic elements inside ngFor) i cannot access the components to modify their checked state.

Related

Child component's EventEmitter "loses" parent component's observer

I have a parent component which observes child component's Output event emitter (topicsChanged).
Parent component:
import {
ChangeDetectionStrategy,
Component,
EventEmitter,
Input,
OnInit,
Output
} from "#angular/core";
import { Language } from "../language";
import { TemplateTopic } from "../template-topic";
#Component({
selector: "at-main-topics",
templateUrl: "./main-topics.component.html",
changeDetection: ChangeDetectionStrategy.OnPush
})
export class MainTopicsComponent implements OnInit {
#Input() templates: string[] = [];
#Input() templateTopics: TemplateTopic[] = [];
#Input() languages: Language[] = [];
#Output() templateTopicChanged = new EventEmitter<TemplateTopic>();
constructor() {}
ngOnInit(): void {}
get availableTemplateTopics(): TemplateTopic[] {
return this.templates
.map(x => +x)
.map(template => {
const existingTopic = this.templateTopics.find(
x => x.template === template
);
return (
existingTopic ||
{ //observer will disappear for this empty created object.
template: template,
topics: []
}
);
});
}
onTopicsChanged(templateTopic: TemplateTopic) {
// This will not be triggered for 3rd template which is created in availableTemplateTopics getter, because it doesn't exist in initial data (templateTopics)
this.templateTopicChanged.emit(templateTopic);
}
}
<at-template-topic *ngFor="let templateTopic of availableTemplateTopics"
[templateTopic]="templateTopic"
[languages]="languages"
(topicsChanged)="onTopicsChanged($event)">
</at-template-topic>
In one strange case, this event emitter loses it's parent component's observer. That is - in child component I am opening a dialog. Before dialog is opened, if I inspect the emitter, the observer is there, but once the dialog is closed, observer is gone.
Child component:
import { Component, EventEmitter, Input, OnInit, Output } from '#angular/core';
import { MatDialog } from '#angular/material/dialog';
import { Language } from '../language';
import { TemplateTopic } from '../template-topic';
import { Topic } from '../topic';
import { TranslationDialogModel } from '../translation-dialog.model';
import { TranslationDialogComponent } from '../translation-dialog/translation-dialog.component';
#Component({
selector: 'at-template-topic',
templateUrl: './template-topic.component.html'
})
export class TemplateTopicComponent implements OnInit {
#Input() templateTopic: TemplateTopic;
#Input() languages: Language[] = [];
#Output() topicsChanged = new EventEmitter<TemplateTopic>();
private dialogTitle: string = 'lorem ipsum'
constructor(
private dialog: MatDialog
) { }
ngOnInit(): void {
}
onCreateTopic(): void {
this.openDialog();
}
onEditTopic(topic: Topic): void {
this.openDialog(topic);
}
private openDialog(topic?: Topic): void {
// this.topicsChanged always has observer at this point
const dialogRef = this.dialog.open(TranslationDialogComponent, {
data: {
pageTitle: this.dialogTitle,
availableLanguages: this.languages,
translations: topic?.translations
} as TranslationDialogModel
});
dialogRef
.beforeClosed()
.subscribe(translations => {
if (translations) {
if (topic) {
topic.translations = translations;
topic.title = translations[0].title;
} else {
this.templateTopic.topics.push({ translations, title: translations[0].title })
}
// When called via onCreateTopic method for a category which was created as an empty placeholder, this.topicsChanged has lost it's observer. However if category had initial data, then observer is still there.
this.topicsChanged.emit(this.templateTopic);
}
})
}
}
There is nothing shady going in the dialog, it simply returns some data and that's it. This is somehow connected to the getter get availableTemplateTopics in parent component from which list of child components are created. In getter there is a list of templates representing each child component which is either populated from already existing data or an empty placeholder is created. And the issue is with the empty placeholder objects.
Snippet:
get availableTemplateTopics(): TemplateTopic[] {
return this.templates
.map(x => +x)
.map(template => {
const existingTopic = this.templateTopics.find(
x => x.template === template
);
return (
existingTopic ||
{ //observer will disappear for this empty created object.
template: template,
topics: []
}
);
});
}
I found that I can solve all of this simply by moving the getter logic one level up, but I would still like to understand this weird behavior. How can observer disappear just like that and how is it connected to the getter?
Stackblitz for full code: https://stackblitz.com/edit/angular-kjewu7?file=src/app/main-topics/main-topics.component.ts

Save toggle state of mat slide toggle

I'm using slide-toggle in a aproject.However I am unbale to save the toggled stae.Everytime I refresh the page,the slide toggle returns to default state instead of remaining in the toggled state.I'm new to angular and don't know what to do.Please help.Thanks in advance.
Here is the ts code:
export class ToggleComponent implements OnInit {
#Output()
change: EventEmitter<MatSlideToggleChange> ;
#Input()
checked: Boolean
ngOnInit() {
}
isChecked = true;
formGroup: FormGroup;
filteringSchedule: boolean ;
toggle:Boolean;
constructor(formBuilder: FormBuilder) {
this.formGroup = formBuilder.group({
enableWifi: false,
acceptTerms: [false, Validators.requiredTrue]
});
}
onFormSubmit(formValue: any) {
alert(JSON.stringify(formValue, null, 2));
}
onChange(ob: MatSlideToggleChange) {
this.filteringSchedule=!this.filteringSchedule;
console.log(!this.filteringSchedule);
}
}
The template code:
<form class="example-form" [formGroup]="formGroup" (ngSubmit)="onFormSubmit(formGroup.value)" ngNativeValidate>
<mat-action-list>
<mat-list-item > <mat-slide-toggle (change)="onChange($event)" [checked]="filteringSchedule" formControlName="enableWifi" >Enable Wifi</mat-slide-toggle></mat-list-item>
<mat-list-item > <mat-slide-toggle formControlName="acceptTerms">Accept Terms of Service</mat-slide-toggle></mat-list-item>
</mat-action-list>
<p>Form Group Status: {{ formGroup.status}}</p>
<button mat-rasied-button type="submit">Save Settings</button>
</form>
Web storages can be used to retain the state of the toggle button (toggled button doesn't change on the page refresh).
Here I've made a Working Stackblitz Demo with the code you have provided for your app. I've used local-storage to achieve the desired feature.
Your ToggleComponent is updated below:-
import { Component, OnInit, Output, EventEmitter, Input } from '#angular/core';
import { MatSlideToggleChange } from '#angular/material';
import { FormGroup, FormBuilder, Validators } from '#angular/forms';
#Component({
selector: 'app-toggle',
templateUrl: './toggle.component.html',
styleUrls: ['./toggle.component.css']
})
export class ToggleComponent implements OnInit {
#Output() change: EventEmitter<MatSlideToggleChange>;
#Input() checked: boolean;
isChecked = true;
formGroup: FormGroup;
filteringSchedule: boolean;
toggle: boolean;
constructor(
private formBuilder: FormBuilder
) { }
ngOnInit() {
this.filteringSchedule = JSON.parse(localStorage.getItem('toggleButtonState'));
this.formGroup = this.formBuilder.group({
enableWifi: this.filteringSchedule,
acceptTerms: [false, Validators.requiredTrue]
});
}
onFormSubmit(formValue: any) {
alert(JSON.stringify(formValue, null, 2));
}
onChange(ob: MatSlideToggleChange) {
this.filteringSchedule = !this.filteringSchedule;
localStorage.setItem('toggleButtonState', JSON.stringify(this.filteringSchedule));
}
}
Also note:- To be consistent for the user across all browsers in addition (or instead of) to storing toggled state of the button in cookies/local storage you can store it server side and when loading the site get the info about the toggled button state from the server.
On a side note form builder code, you have placed inside the constructor should be kept inside ngOnInit, to avoid performance issues with the app.
Hope this is helpful.
Store your toggle property in localStorage and then you can retrieve localStorage value on ngOnInit method.
You can even use sessionStorage based on your requirement. Use of localStorage and sessionStorage is almost identical, you just need to replace one with another.
onChange(ob: MatSlideToggleChange) {
this.filteringSchedule = !this.filteringSchedule;
localStorage.setItem('yourKey', JSON.stringify(this.filteringSchedule));
}
ngOnInit() {
this.filteringSchedule = localStorage.getItem('yourKey') == 'true';
}

How to add event to input in Angular 5 app using directive?

I have the following directive groupingFormat which perform grouping to an input text
when user use the key up:
#Directive({
selector: '[groupingFormat]'
})
export class GroupingFormatDirective {
private el: HTMLInputElement;
constructor(elRef: ElementRef) {
this.el = elRef.nativeElement;
}
ngAfterViewInit(): void {
let elem : HTMLInputElement = this.el;
elem.addEventListener('keyup',() => {
this.el.value = this.digitGrouping(this.el.value);
});
}
}
Example of usage:
<input type="text" #myValue="ngModel" name="my_value" [(ngModel)]="myObj.myValue" id="my_value" required groupingFormat>
This directive is working as expected but now I have new requirement: The input
text should use the directive also when the page is load and also if the a form
is open inside the page with the input becoming visible.
Is there an easy way to update the directive to support this functionality or
any alternative solution? Attach another directive ?
Thanks.
<input type="text" name="my_value" [appInputevent]="myValue" [(ngModel)]="myValue">
directive file
import { Directive,HostListener,ElementRef, Input } from '#angular/core';
#Directive({
selector: '[appInputevent]'
})
export class InputeventDirective {
constructor(private el:ElementRef) { }
#Input('appInputevent') params: string;
#HostListener('keyup', ['$event'])
onKeyUp(event: KeyboardEvent) {
console.log('got parameters: '+this.params);
}
private highlight(color: string) {
this.el.nativeElement.style.backgroundColor = color;
}
}
change hostlistner event according to your need.
For demonstration-- https://stackblitz.com/edit/ionic2-test?file=app%2Finputevent.directive.ts

Angular: Blur and empty a form field on click without jQuery?

I have a form input which I would like to blur (de-focus) and empty when a button is clicked.
In AngularJS, I did this in the controller like so:
angular.element('#search-input').val('');
angular.element('#search-input').blur();
In Angular (4.4.4) I have it working like so:
$('#search-input').val('');
$('#search-input').blur();
But I'd rather not use jQuery. What's the proper way to do this in Angular?
Here's the whole component:
import { Component, OnInit } from '#angular/core';
#Component({
selector: 'pb-header',
templateUrl: './header.component.html'
})
export class HeaderComponent implements OnInit {
private searchActive: boolean;
constructor() {
this.searchActive = false;
}
ngOnInit() {
}
toggleSearch = function () {
if (this.searchActive) {
this.searchActive = false;
$('#search-input').val('');
$('#search-input').blur();
} else {
this.searchActive = true;
}
};
}
You can use NgForms reset method:
#ViewChild(NgForm)
public form:NgForm;
form.reset(<value>);
Also there is no need for blur, since you are clicking button and it will take focus, if it does not, set tabindex attribute of the button.

How to trigger change() in a angular form by a custom control without an input

I do want to create a custom control which does not include any input. Whenever the control changes, I do want to save the complete form.
Our current approach uses the form-changed-event like this:
<form #demoForm="ngForm" (change)="onChange()">
<custom-input name="someValue" [(ngModel)]="dataModel">
</custom-input>
</form>
As you can see, we use the "change"-event to react to any change in the form.
This works fine as long as we have inputs, checkboxes, ... as controls.
But our custom control does only exist out of a simple div we can click on. Whenever I click on the div the value of the control is increased by 1. But the "change"-event of the form is not fired. Do I somehow have to link my custom control to the form? Or are there any events which need to be fired?
import { Component, forwardRef } from '#angular/core';
import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '#angular/forms';
#Component({
selector: 'custom-input',
template: `<div (click)="update()">Click</div>`,
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CustomInputComponent),
multi: true
}]
})
export class CustomInputComponent implements ControlValueAccessor {
private onTouchedCallback: () => void = () => {};
private onChangeCallback: (_: any) => void = () => {};
update(){
this.value++;
}
get value(): any {
return this.innerValue;
};
set value(v: any) {
console.log("Change to");
if (v !== this.innerValue) {
this.innerValue = v;
this.onChangeCallback(v);
}
}
writeValue(value: any) {
if (value !== this.innerValue) {
this.innerValue = value;
}
}
registerOnChange(fn: any) {
this.onChangeCallback = fn;
}
registerOnTouched(fn: any) {
this.onTouchedCallback = fn;
}
}
I've created a plunker to demonstrate the problem:
https://plnkr.co/edit/ushMfJfcmIlfP2U1EW6A
Whenever you click on "Click" the model-value is increased, but there is no output on the console, as the change-event is not fired... (There is a console.log linked to the change-event)
Thanks for your replies.
Finally I found the following solution to this problem:
As Claies mentioned in the comment, my custom component does not fire the change event. Therfore the form does never know about the change. This has nothing todo with angular, but as said is the expected behaviour of a input/form.
The easiest solution is to fire the change-event in the customcontrol when a change happens:
constructor(private element: ElementRef, private renderer: Renderer) {
}
public triggerChanged(){
let event = new CustomEvent('change', {bubbles: true});
this.renderer.invokeElementMethod(this.element.nativeElement, 'dispatchEvent', [event]);
}
That's it, whenever I called "onControlChange(..)" in my custom component, then I fire this event afterward.
Be aware, that you need the Custom-Event-Polyfill to support IE!
https://www.npmjs.com/package/custom-event-polyfill
You need to emit the click event of div to its parent. so that you can handle the event.
Plunker Link
Parent component:
import { Component, forwardRef, Output, EventEmitter } from '#angular/core'; // add output and eventEmitter
import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '#angular/forms';
#Component({
selector: 'custom-input',
template: `<div (click)="update($event)">Click</div>`,
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CustomInputComponent),
multi: true
}]
})
export class CustomInputComponent implements ControlValueAccessor {
private onTouchedCallback: () => void = () => {};
private onChangeCallback: (_: any) => void = () => {};
#Output() clickEvent = new EventEmitter(); // add this
update(event){
this.value++;
this.clickEvent.emit(event); // emit the event on click event
}
get value(): any {
return this.innerValue;
};
}
child component:
//our root app component
import {Component} from '#angular/core'
#Component({
selector: 'demo-app',
template: `
<p><span class="boldspan">Model data:</span> {{dataModel}}</p>
<form #demoForm="ngForm">
<custom-input name="someValue"
[(ngModel)]="dataModel" (clickEvent) = onChange()> // handling emitted event here
Write in this wrapper control:
</custom-input>
</form>`
})
export class AppComponent {
dataModel: string = '';
public onChange(){
console.log("onChangeCalled");
}
}
Thanks Stefan for pointing me in the right direction.
Unfortuantely Renderer (which has invokeElementMethod()) has recently been deprecated in favor or Renderer2 (which does not have that method)
So the following worked for me
this.elementRef.nativeElement.dispatchEvent(new CustomEvent('change', { bubbles: true }));
It seems that change event is not fired on form when you call ControlValueAccessor onChange callback (callback passed in registerOnChange function), but valueChanges observable (on the whole form) is triggered.
Instead of:
...
<form (change)="onChange()">
...
you can try to use:
this.form.valueChanges
.subscribe((formValues) => {
...
});
Of course, you must get proper form reference in your component.

Categories

Resources