Angular: Hiding/showing elements by ngIf or Toggling a class? - javascript

I'm wondering what the best way to approach this problem is and I'm very new to TypeScript and Angular. Using Angular 5.
Anyway. I have a list of elements on a page via a table.
This is the code that controls said list.
<tbody>
<tr class="text-center" *ngFor="let topic of topics">
<td *ngIf="!editTopicMode">{{ topic.name }}</td>
<td id="{{topic.id}}" *ngIf="editTopicMode">
<form>
<div class="form-group">
<input class="form-control" type="text" name="name" value="{{topic.name}}" />
</div>
</form>
</td>
<td>
<div *ngIf="!editTopicMode" class="btn-group btn-group-sm">
<button class="btn btn-link" (click)="editTopicBtnClick(topic.id)">
<i class="fa fa-pencil fa-2x" aria-hidden="true"></i>
</button>
<button class="btn btn-link">
<i class="fa fa-trash fa-2x" aria-hidden="true"></i>
</button>
</div>
<div *ngIf="editTopicMode" class="btn-group-sm">
<button class="ml-2 btn btn-sm btn-outline-secondary" (click)="cancelEditMode()">Cancel</button>
<button class="ml-2 btn btn-sm btn-outline-primary">Save</button>
</div>
</td>
</tr>
</tbody>
What I'm aiming to do is that if a user clicks on the pencil(edit) icon, then the adjacent div changes from just a regular td to an input and the edit/delete buttons to change to a cancelEdit/save edits button group. (I know that I need to change the html a bit because the buttons aren't in the form element currently, but I'm not there on the wiring it up part).
I've thought of two ways to do this. 1) with ngIf's so that I can conserve the elements that are rendered and the edit/cancel buttons toggle the editMode; or 2) use ngClass and toggle display:none css classes for the button clicked.
Right now, when you click the edit button, regardless of which edit button you click, it flips all the columns to inputs, rather than just the row the user wants to edit.
Here's my component ts:
import { Component, OnInit, TemplateRef, ElementRef, ViewChild, Inject } from '#angular/core';
import { Topic } from '../models/topic';
import { TopicService } from '../services/topicService/topics.service';
import { AlertifyService } from '../services/alertify/alertify.service';
import { ActivatedRoute } from '#angular/router';
import { DOCUMENT } from '#angular/common';
#Component({
selector: 'app-topics',
templateUrl: './topics.component.html',
styleUrls: ['./topics.component.css']
})
export class TopicComponent implements OnInit {
#ViewChild('topicId') topicId: ElementRef;
topics: Topic[];
newTopic: Topic = {
id: 0,
name: '',
};
editTopicMode = false;
constructor(
#Inject(DOCUMENT) document,
private topicsService: TopicService,
private alertify: AlertifyService,
private route: ActivatedRoute
) { }
ngOnInit() {
//this.route.data.subscribe(data => {
// this.topics = data['topics'];
//})
this.getTopics();
}
getTopics() {
this.topicsService.getAllTopics()
.subscribe(data => {
this.topics = data;
}, error => {
this.alertify.error(error);
});
}
addTopic() {
this.topicsService.createTopic(this.newTopic)
.subscribe((topic: Topic) => {
this.topics.push(topic);
this.alertify.success(this.newTopic.name + ' added as a new topic.');
this.newTopic.name = '';
},
(err: any) => {
this.alertify.error(err);
}
)
}
editTopicBtnClick(event) {
console.log(event);
this.editTopicMode = true;
console.log(document.getElementById(event));
}
cancelEditMode() {
this.editTopicMode = !this.editTopicMode;
}
}
Any thoughts on the best (most efficient) way to make this happen?

You've done all the hard work already.
For single item editing, all that's left is: change editTopicMode to something like editTopicId.
Then you can:
Set it to topic.id on edit enabled, and null for example on edit closed
Change your *ngIf to editTopicId === topic.id (or !== as needed)
And that should be all.
If you want to enable multiple editing, just add a property called isInEditMode to each topic.
Your *ngIf check becomes topic.isInEditMode
No isInEditMode property at all is just like false, because undefined is a falsy value
Set topic.isInEditMode to true on editing enabled, false on editing closed

Using *ngIf is fine just make sure that you set the *ngIf variable to point to somethign specific to the particular row of the *ngFor
this example could be cleaner but you could accomplish it as simply as
<button (click)="topic.edit = true" *ngIf="topic.edit === false">edit</button>
<button (click)="topic.edit = false" *ngIf="topic.edit === true">cancel</button>

Related

Angular: How to mark a validator as dirty manually

I have a component I would like to mark as dirty when the "Next" button in my stepper component is clicked.
Currently inside my stepper.component.ts I have a function displayValidation, which is called when my Next button is clicked and manually marks all form inputs as touched (I have also double checked each's status to make sure touched == true when the Next button is clicked):
displayValidation(field: FormlyFieldConfig) {
if (field?.fieldGroup){
field.fieldGroup.forEach((curFieldGroup) => {
curFieldGroup.formControl.markAsTouched();
this.setFieldTypeInvalid(curFieldGroup.type, curFieldGroup.formControl.status);
});
}
}
This successfully changes all my components to touched, however for number input component, it does nothing visually (expecting a red outline/dirty) despite touched now equalling true, and validation existing for the component (called currency component).
Here is my currency.component.ts in question:
import { Component } from '#angular/core';
import { CurrencyPipe} from '#angular/common';
import { FieldType } from '#ngx-formly/core';
import { FormGroup, FormControl, Validators } from '#angular/forms';
#Component({
selector: 'app-formly-field-currency',
templateUrl: './currency.component.html'
})
export class FormlyFieldCurrencyComponent extends FieldType {
constructor(private currencyPipe: CurrencyPipe) {
super();
}
currencyGroup = new FormGroup({
currencyForm: new FormControl('', [
Validators.required
])
});
get currencyAmount(){
return this.currencyGroup.get('currencyForm');
}
}
In my corresponding currency.component.html, I have formGroup and formControl validation that are fired when the currency component is clicked manually, but I would like it to fire in the above stepper.component.ts such that it is visually dirty when the Next button is clicked:
currency.component.html:
<div class="input-group mb-3">
<form [formGroup]="currencyGroup">
<div class="input-group mt-2 flex-nowrap">
<div class="input-group-prepend">
<span class="input-group-text">$</span>
</div>
<input matInput
[class.is-invalid] = "currencyGroup.get('currencyForm').invalid && currencyGroup.get('currencyForm').touched"
type="number"
class="form-control"
[formControl]="formControl"
formControlName="currencyForm" #input
[formlyAttributes]="field"
>
</div>
<div
class="mx-auto"
*ngIf="(currencyAmount.invalid && currencyAmount.touched) || currencyAmount.dirty">
<small *ngIf="currencyAmount.errors?.required" class="text-danger">
Currency amount is required.
</small>
</div>
</form>
</div>
I understand this is a lot here, however I feel as if I am so close, so any help would be greatly appreciated.
Thanks!

How to open modal for multiple components from same parent component

I am using angular 9.
I have a home page which has four buttons. I want to open separate modal on each button's click at a time.
I did research a lot, here is my trial and effort.
Thanks in advance :)
parent component
#Component({
selector: 'app-link-budget-view',
templateUrl: './home.component.html',
styleUrls: ['./home.component.scss']
})
export class HomeComponent implements OnInit {
#ViewChild('childOne') callChild_OneFn: ChildOneComponent;
#ViewChild('childTwo') callChild_TwoFn: ChildTwoComponent;
...
ngOnInit(): void {
}
openModalOne() {
this.callChild_OneFn.OpenModalFunction();
}
openModalOne() {
this.callChild_TwoFn.OpenModalFunction();
...
}
}
Home Html
<button class="btn btn-primary mb-2 mr-2" (click)="openModalOne()">Modal 1</button>
<button class="btn btn-primary mb-2 mr-2" (click)="openModalTwo()">Modal 2</button>
...
<app-child-one #childOne></app-child-one>
<app-child-two #childTwo></app-child-two>
...
childOne Component
#Component({
selector: 'app-link-budget-view',
templateUrl: './child-one.component.html',
styleUrls: ['./chile-one.component.scss']
})
export class ChildOneComponent implements OnInit {
constructor( private modalService: NgbModal) { }
ngOnInit(): void {
}
OpenModalFunction() {
console.log("component One function running...")
this.modalService.open('#ModalOneId', { size: 'xl' });
}
...
}
similarly there is a function in component two
OpenModalFunction() {
console.log("component Two function running...")
this.modalService.open('#ModalTwoId', { size: 'xl' });
}
Component One Html
<ng-template #ModalOneId let-modal>
<div class="modal-header">
<h4 class="modal-title">This is modal one</h4>
<button type="button" class="close" aria-label="Close" (click)="modal.dismiss('Cross click')">
<span aria-hidden="true">Γ—</span>
</button>
</div>
<div class="modal-body">
<div class="card">
<div class=" card-body">
<div id="table" class="table-editable">
...
...
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-light" (click)="modal.close('Close click')">Close</button>
</div>
</ng-template>
similarly ng-templete is used in component two html
you didn't mention if what you wrote is working or not, or what errors you are getting. it looks like it should work to me. Although if you just want to open a modal, you can handle it completely in your html:
<button (click)="childOne.OpenModalFunction()">Modal 1</button>
<button (click)="childTwo.OpenModalFunction()">Modal 2</button>
<app-child-one #childOne></app-child-one>
<app-child-two #childTwo></app-child-two>
that should be the only thing you need in the parent. no code needed in the parent .ts file. if this is not working, there's something wrong with the code in your child class.
Finally I fixed it here is the code to help others
Parent ts
export class ParentComponent implements OnInit {
constructor(private modalService: CustomModalServiceService) { }
ngOnInit(): void {
}
openModal(modalName: string) {
this.modalService.openModalFunction(modalName);
}
}
Parent html
<button class="btn btn-primary mb-2 mr-2" (click)="openModal('childOne')">Modal_1</button>
<button class="btn btn-primary mb-2 mr-2" (click)="openModal('childTwo')">Modal_2</button>
child one or two ts code accordingly for one or two
export class ChildOneComponent implements OnInit {
constructor(private modalRef: NgbActiveModal) { }
ngOnInit(): void {
}
hideModalFunction() {
this.modalRef.close();
}
}
child one or two html for modal
<div class="modal-header">
<h4 class="modal-title">This is modal one</h4>
<button type="button" class="close" aria-label="Close" (click)="hideModalFunction()">
<span aria-hidden="true">Γ—</span>
</button>
</div>
<div class="modal-body">
<div class="card">
<div class=" card-body">
<div id="table" class="table-editable">
...
...
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-light" (click)="hideModalFunction()">Close</button>
</div>
and you need a custom modal service
Custom modal service
export class CustomModalService {
...
private modalRef: NgbModalRef;
constructor( private modalService: NgbModal) { }
ngOnInit(): void {
}
openModalFunction(modalName: string) {
switch(modalName) {
case 'one':
console.log("component One function running...");
// do any execution before opening child component
this.modalRef = this.modalService.open(ChildOneComponent, { size: 'xl' });
this.modalRef.componentInstance.testData = 'test';
break;
case 'two':
console.log("component Two function running...");
// do any execution before opening child component
this.modalRef = this.modalService.open(ChildTwoComponent, { size: 'xl' });
break;
case default:
// do nothing
}
}
}
You are repeating too much code here. You could make it more re-usable and less repetitive. Try to extract most of the business logic to a service. Leave the component as dumb(no business logic) as possible for better re-usability.
Parent component
#Component({
selector: 'app-link-budget-view',
templateUrl: './home.component.html',
styleUrls: ['./home.component.scss']
})
export class HomeComponent implements OnInit {
...
constructor(private myModalService: MyModalService)
ngOnInit(): void {
}
openModal(modelName: string) {
this.myModalService.openModalFunction(modalName);
}
}
Home Html
<button class="btn btn-primary mb-2 mr-2" (click)="openModal('one')">Modal 1</button>
<button class="btn btn-primary mb-2 mr-2" (click)="openModal('two')">Modal 2</button>
...
MyModal Service
#Injectable({
providedIn: 'root'
})
export class MyModalService {
...
private modalRef: NgbModalRef;
constructor( private modalService: NgbModal) { }
ngOnInit(): void {
}
openModalFunction(modalName: string) {
switch(modalName) {
case 'one':
console.log("component One function running...");
// do any execution before opening child component
this.modalRef = this.modalService.open(ChildOneComponent, { size: 'xl' });
this.modalRef.componentInstance.testData = 'test';
break;
case 'two':
console.log("component Two function running...");
// do any execution before opening child component
this.modalRef = this.modalService.open(ChildTwoComponent, { size: 'xl' });
case default:
// do nothing
}
}
hideModalFunction() {
//do something before closing the modal
this.modalRef.close();
}
}
Now no OpenModalFunction() required inside the child components as you have extracted the modal opening logic into the MyModalService. This will also adhere to the Single Functionality principle means every component has only single functionality to be fulfilled. Furthermore, you have now full control via MyModalService to handle popups. If you want to execute a common code before/after opening/closing the windows like notifying another service, you could do that.
Now this could be further optimized as all software codes do. Like extracting common code from Child Components to a single interface/abstract class. But it totally depends upon your requirements.

Angular, how to use directive based on an boolean

I use a ngFor to show different button. I need to use a directive on different button but not all.
So I make this code :
<div *ngFor="let btn of btns">
<button {{ btn.useDirective ? 'appMyDirective' : '' }} ></button>
</div>
But I get this error
Error: Template parse errors:
Unexpected closing tag "button". It may happen when the tag has already been closed by another tag.
update you directive to be trigger base on state as an example consider this πŸ‘‡
#Directive({
selector: '[appHello]'
})
export class HelloDirective {
#Input() appHello:boolean;
constructor(private elem: ElementRef, private renderer: Renderer2) { }
ngAfterViewInit() {
if (this.appHello !== false ) {
this.renderer.setProperty(this.elem.nativeElement, 'innerHTML', 'Hi 😎');
}
}
}
template
<div *ngFor="let btn of btns">
<button [appHello]="btn.useDirective">Hi </button>
</div>
if you set the value to be true the directive will work otherwise nothing will happen
demo πŸ”₯πŸ”₯
Your syntax is invalid. To archieve what you want, do something like this:
<div *ngFor="let btn of btns">
<button *ngIf="btn.useDirective" appMyDirective></button>
<button *ngIf="!btn.useDirective"></button>
</div>
Try using below instead.
<div *ngFor="let btn of btns">
<button appMyDirective *ngIf="btn.useDirective"></button>
<button *ngIf="!btn.useDirective"></button>
</div>

On a Keydown.enter event in input element within a child component is also calling a method that is defined in Parent Component

I have a parent component where user can select skills from a range of options and a child component where user can add their own skill if its not available on the parent component.
The issue is in child component, when a user enters skill into an input element on which I have an #keydown.enter event defined to call a method, to take the input and push it to an array and that all works. The only problem is when keydown.enter event is fired it's also calling a method that is defined in the parent component which changes the state of the options element.
// parent component
<div class="card-body">
<p class="card-subtitle font-weight-bold mb-1">Select Skills</p>
<button
v-for="skill in skills"
:key="skill.id"
:class="[skill.state ? skillSelectedState : skillNotSelectedState]"
class="btn btn-sm m-2" #click="addSkill(skill)"
:value="skill.category">
{{skill.skills}}
</button>
<clientInput></clientInput> // child component
</div>
<script>
import clientInput from './ClientSkillInput.vue'
export default {
data() {
return {
skills: [], // getting skills from an axios call
selectedSkills: [],
}
}
}
methods: {
addSkill(skill) { // this is the method getting called
if (!skill.state) {
this.selectedSkills.push(skill.skills);
skill.state = true;
} else {
let position = this.selectedSkills.indexOf(skill.skills);
this.selectedSkills.splice(position, 1);
// skill.state = false;
}
},
}
// child component
<template>
<div class="form-group mt-2">
<label class="d-block">Not what you're looking for?</label>
<div class="customWraper">
<div class="userSkillbox d-inline bg-secondary"> // will be using v-for to loop on userProvidedSkills and show the user inputs
Rrby on rails
<button class="userSkillboxBtn btn-sm border-0 text-white"> // to remove the input item
<i class="fas fa-times"></i>
</button>
</div>
<input v-model="userInput" type="text" class="d-inline border-0" placeholder="Type to add different skill" #Click="">
</div>
</div>
</template>
<script>
export default {
data() {
return {
isEditable: true,
userInput: '',
userProvidedSkills: [],
}
},
methods: {
addUserInput() {
this.userProvidedSkills.push(this.userSkill);
this.userSkill = '';
}
}
}
</script>
It is not clear where you’re adding the keydown event, but there 2 possible solutions:
1.Use a event modifier on the input to stop propagation
<input #keydown.enter.stop
2.Use the self event modifier on the parent component button
<button
v-for="skill in skills"
#click.self="addSkill(skill)"
:value="skill.category">
{{skill.skills}}
More about event modifiers here

How to fix <!--bindings={ "ng-reflect-ng-for-of": "" }-->

Im new to Angular. I am creating a button inside game-control component and using event binding and property binding. When i click the button numbers will be entered into an array continuously using setInterval method. I am passing the data between one component to another. The game-control component's selector is called inside the app component. The button worked fine with the click event but when i used the ngFor in order to iterate through the array and display the buttons also did not appear in the dom. Thanks in advance
game-control.component.html
<div class="row">
<div class="col-md-12">
<button
class="btn btn-primary"
type="button"
(click)="gameStart()">Start</button>
<button
class="btn btn-danger"
type="button"
(click)="gameStop()">Stop</button>
<br>
<p>{{element}}</p>
</div>
</div>
game-control.component.ts
export class GameControlComponent implements OnInit {
#Input() element:number;
#Output() createNumber= new EventEmitter<number>();
constructor() { }
ngOnInit() {
}
gameStart()
{
this.createNumber.emit(this.element);
}
}
app.component.html
<div class="container">
<div class="row">
<div class="col-md-12">
<app-game-control
(createNumber)='onStart()'
*ngFor="let myElement of myHoldings"
[element]="myElement"
>
</app-game-control>
</div>
</div>
</div>
app.component.ts
export class AppComponent {
myHoldings=[];
onStart()
{
setInterval(()=>{
this.myHoldings.push(this.myHoldings.length+1);
console.log("hello");
},1000);
}
}
The solution is to have an internal variable in the component that is assigned from the Input() variable
#Input() element:number;
internalNumber : number ;
ngOnInit() {
this.internalNumber = this.number;
}
And then use that variable to do bindings or emit events
For a similar question that might help
Click function not being called for a mat-menu with items generated with ngFor

Categories

Resources