I have a parent which is a form.
This form is composed of two child components:
experiment create (parent)
creation-dataset (child)
creation-metadata (child)
I use a angular component -> mat-accordion to navigate through the two children components.
I use #Input to have the result of what is filled in the children component into the parent.
I want to submit the form only if a file is chosen for both of them. Therefore, I set a variable (in datasetList[i].fileValid) to say whether a file has been selected. Like this I disabled the button if a file is not updated. To disable the button I called the two function:
isDatasetFilesValid()
isMetadataFilesValid()
However, when the variable changed for the second child component it does not updated the disabled button.
This works, only if I press "previous" and "next". The button is not disabled anymore. Like if I needed to reload or refresh the parent. Maybe because of the life cycle ?
Parent Component:
export class ExperimentCreateComponent implements OnInit {
data: any = {};
datasetList: any = [{ fileValid: false }];
metadataList: any = [{ fileValid: false }];
// Functions to navigate through the expansion panels
setStep(index: number) {
this.step = index;
}
nextStep() {
this.step++;
}
prevStep() {
this.step--;
}
isDatasetFilesValid() {
return this.datasetList.findIndex(function(item, i) {
return item.fileValid == false;
});
}
isMetadataFilesValid() {
return this.metadataList.findIndex(function(item, i) {
return item.fileValid == false;
});
}
}
Parent HTML:
<div class="jumbotron">
<div class="container">
<div class="row">
<div class="col-sm-8 offset-sm-2">
<form name="form" (ngSubmit)="f.form.valid" #f="ngForm" novalidate>
<mat-accordion class="headers-align">
<mat-expansion-panel id="datasetUpload" [expanded]="step === 0" (opened)="setStep(1)" hideToggle="true">
<app-creation-dataset [datasetList]="datasetList"></app-creation-dataset>
<mat-action-row>
<button mat-button color="warn" (click)="prevStep()">Previous</button>
<button mat-button color="primary" (click)="nextStep()">Next</button>
</mat-action-row>
</mat-expansion-panel>
<mat-expansion-panel id="metadataUpload" [expanded]="step === 1" (opened)="setStep(2)" hideToggle="true">
<app-creation-metadata [metadataList]="metadataList"></app-creation-metadata>
<mat-action-row>
<button mat-button color="warn" (click)="prevStep()">Previous</button>
<button mat-button color="primary" type="submit" [disabled]="(isMetadataFilesValid() != -1) && (isDatasetFilesValid() != -1)" (click)="createExperiment()">End</button>
</mat-action-row>
</mat-expansion-panel>
</mat-accordion>
</form>
</div>
</div>
</div>
</div>
Child Component:
export class CreationDatasetComponent implements OnInit {
#Input() datasetList: any = [{ fileValid: false }];
fileSelected: File;
constructor(private papa: Papa, private cd: ChangeDetectorRef) {}
ngOnInit() {}
onChange(files: FileList, index: number, dom: any) {
// Option to parse the file with papaparse
let options = {
header: true,
error: (err, file) => {
this.datasetList[index].fileValid = false;
alert(
"Unable to parse CSV file, please verify the file can be accessed and try again. Error reason was: " +
err.code
);
return;
},
complete: (results, file) => {
console.log("Parsed:", results, file);
let filename = file.name;
// Add the dataset to the datasetList
this.datasetList[index].headers = results.meta.fields;
this.datasetList[index].values = results.data;
this.datasetList[index].filename = filename;
this.datasetList[index].is_metadata = false;
this.datasetList[index].fileValid = true;
this.cd.detectChanges();
}
};
this.fileSelected = files[0]; // Get the file
// Call the function to parse the file, option is the callback
this.papa.parse(this.fileSelected, options);
}
// Add a dataset form
addDataset() {
this.datasetList.push({ fileValid: false });
}
// Remove a dataset form
removeDataset(index: number) {
this.datasetList.splice(index, 1);
}
}
Child HTML:
<div *ngFor="let dataset of datasetList; let index = index">
<div id="datasetFiles">
<h6>Select the type of dataset and browse the files:</h6>
<div class="container">
<div class="row justify-content-between">
<div class="col-6 d-flex align-items-center">
<input id="file" #file (change)="onChange(file.files, index, $event.currentTarget)" type="file">
</div>
</div>
</div>
</div>
</div>
<div>
<button mat-icon-button color="primary" (click)="addDataset()">
<mat-icon>add_box</mat-icon>
</button>
</div>
So, to make this answer more clear, read comments on question.
I'm going to past the example for the #Output:
this is the CHILD.COMPONENT.TS
#Component({
selector: 'children',
templateUrl: './children.component.html',
styleUrls: ['./children.component.scss'],
providers: [{...
})
})
export class ChildrenComponent {
#Output() editedEmitter = new EventEmitter<number>();
private variableToPass = 10;
constructor() {}
functionToCall() {
this.editedEmitter.emit(20);
}
this is the PARENT.COMPONENT.HTML
<section>
<children (editedEmitter)="updateValue($event)"></children>
</section>
<!-- in the component you'll do
updateValue(val: number) {
this.variableToUpdate = val;
}
-->
[disabled] requires a condition (true or false), the code you put in: isMetadataFilesValid() != -1 isDatasetFilesValid() != -1 is not a proper condition, they are two. If you want both conditions to be true, use the && notation.
[disabled]="(isMetadataFilesValid() != -1) && (isDatasetFilesValid() != -1)"
Alternatively I suggest moving the condition to the functions themselves so they return a boolean.
Related
I am trying to create a simple CRUD app with Vuejs 3.
I have a homepage with a form (as a child component) and a table with created items (as another child component). I submit data via the form to API/database and the table updates. So far so good.
Then, for the update phase, I would like to have a detail page for each item where I also would have the form (the same component reused). But the idea is that form fields would be pre-populated with data from API/Database.
The table on the homepage has a route-link to a detail page and I am passing the id of the item as params. The detail page makes request to API based on id, receives item data and passes them as props into the form component.
If I try to render data directly into template like this, it works fine:
<p v-if="submitType === 'update' && item.id">{{ item.id }}</p>
Now, form fields are bound by v-model to data (form.id for example). But when I try to repopulate it as below, I always get undefined values.
data() {
return {
form: {
id: this.submitType === 'update' ? this.item.id : 0,
}
}
},
I suspect that problem is that the parent call to API is asynchronous and the passing of props is delayed. Because when I pass as props some hardcoded value, it appears as a value in the form field with no problem. Also if the form is shown only when props are received (with the v-if directive), the data.form.id is still undefined.
So is there any way how to pre-populate bound form fields with received props and still have the form component reused for insert and update actions? The rest of the relevant code is below. Thank you very much in advance
// Detail Page
<template>
<Form :item="item" submit-type="update"></Form>
</template>
<script>
export default {
data() {
return {
item: {}
}
},
created() {
callAPI(id).then( response => this.item = response.data )
}
}
</script>
// Form Component
<template>
<p v-if="submitType === 'update' && item.id">{{ item.id }}</p>
<div v-if="submitType === 'insert' || (submitType === 'update' && item.id )">
<section>
<form #submit.prevent="onSubmit">
<div>
<label for="id">ID</label>
<input id="id" name="id" v-model="form.id" type="number" placeholder="ID">
</div>
<input type="submit" value="Save">
</form>
</section>
</div>
</template>
<script>
export default {
name: 'Form',
props: {
item: {
type: Object
},
submitType: {
type: String
}
},
data() {
return {
form: {
id: this.submitType === 'update' ? this.item.id : 0,
}
}
},
}
</script>
You can try with watchers, take a look at following snippet:
const app = Vue.createApp({
data() {
return {
item: {},
type: 'update'
}
},
methods: {
change() {
this.type === 'update' ? this.type = 'insert' : this.type = 'update'
}
},
created() {
fetch('https://jsonplaceholder.typicode.com/todos/1')
.then(response => response.json())
.then(json => this.item = json)
//callAPI(id).then( response => this.item = response.data )
}
})
app.component('myForm', {
template: `
<p v-if="submitType === 'update' && item.id">{{ item.id }}</p>
<div v-if="submitType === 'insert' || (submitType === 'update' && item.id )">
<section>
<form #submit.prevent="onSubmit">
<div>
<label for="id">ID</label>
<input id="id" name="id" v-model="form.id" type="number" placeholder="ID">
</div>
<input type="submit" value="Save">
</form>
</section>
</div>
`,
props: {
item: {
type: Object
},
submitType: {
type: String
}
},
data() {
return {
form: {}
}
},
methods: {
fillData() {
this.submitType === 'update' ? this.form = {...this.item} : this.form = {id: 0}
}
},
watch: {
item() {
this.fillData()
},
submitType() {
this.fillData()
}
},
})
app.mount('#demo')
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>
<div id="demo">
<button #click="change">switch type</button>
{{type}}
<my-form :item="item" :submit-type="type"></my-form>
</div>
Here's the code:
parent.ts
newInputName = '';
inputName = '';
addName = false;
constructor() {
private modal: NzModalService) { }
ngOnInit() {
this.modal.afterAllClose.subscribe(md => {
console.log(this.addArea)
});
}
addItem(): void {
this.inputName = this.name;
this.newInputName = '';
this.addName = true;
}
parent.html
<div class="modal-body">
<label class="pb-2">Name</label>
<input nz-input class="mr-2" [(ngModel)]="newInputName" placeholder="Type name...">
</div>
<nz-divider></nz-divider>
<section class="form-footer" nz-row>
<div nz-col>
<button class="mr-1" nz-button nzType="primary" type="button" (click)="addItem()"
[disabled]="newInputName.length <= 0">Add Name</button>
<button nz-button class="btn-secondary" type="button" (click)="modalRef.close()"
[textContent]="'Cancel' | translate"></button>
</div>
</section>
child.ts
#Input() addName: boolean;
editLayout(shopFloor?: any, lines?: any) {
this.modalRef = this.modal.create({
......
.....
.....
.......
.......
});
this.modal.afterAllClose.subscribe((x: any) => {
this.addName = false;
});
}
What I'm trying to do here is after submitted, the modal will be closed then the value addArea will be changed to false if the value of addName is true.
cause I'm having trouble when I try to save/submit the value still true when I try to change the value of addArea to true.
I also tried to do like this:
parent.html
<app-child [addNewName]="addName"></app-child>
child.ts
#Input() addName: boolean;
#Output()
addNameChange = new EventEmitter<boolean>();
#Input() addName: boolean;
editLayout(shopFloor?: any, lines?: any) {
this.modalRef = this.modal.create({
......
.....
.....
.......
.......
});
this.modal.afterAllClose.subscribe((x: any) => {
this.addNameChange.emit(!this.addName);
});
}
But still it doesn't work.
You were almost there, on top of what you already tried (adding event emitter and emitting when the modal is done and ready to tell the parent component what to do), you also have to tell the parent component to do something when the child emits, adding the following to your parent html template:
<app-child [addNewName]="addName" (addNameChange)="resetNameChange($event)"></app-child>
and then in your parent .ts file:
...
resetNameChange(val) {
...do logic here, probably this.addName = val;
}
I have a component where I have created a group of buttons. The user should be able to select multiple buttons - when selected the value of those buttons is added to an array and when deselected removed from the array.
If the user selects the 'all' button the other buttons are unselected.
I have a running version of this code on stackblitz however the full functionality is not as expected.
https://stackblitz.com/edit/angular-ivy-je18ji?file=src%2Fapp%2Fapp.component.html
Mock Data used retrieved by service
{
"name": "mockClass",
"label": "mockLabel",
"attributes": [
{
"name": "Button1"
},
{
"name": "Button2"
},
{
"name": "Button3"
},
{
"name": "Button4"
},
{
"name": "Button5"
},
{
"name": "Button6"
}
]
}
Data Service component
#Injectable({
providedIn: 'root'
})
export class DataClassService {
getMockDataClass() {
return metadata;
}
}
Exported const is just a simple string
export const SEARCH_BUTTON_GROUP_ALL = 'all';
TS Component
export class AppComponent implements OnInit {
#Input() dataClassName: string;
lame = "Angular " + VERSION.major;
selectedAttributes = ["all"];
dataClass: DataClass;
searchButtonGroupAttributes: any;
constructor(private dataClassService: DataClassService) {}
ngOnInit() {
this.dataClass = this.dataClassService.getMockDataClass();
this.searchButtonGroupAttributes = this.dataClass.attributes;
}
searchButtonGroupClick(attributeName: string) {
if (this.selectedAttributes.indexOf(CONST.SEARCH_BUTTON_GROUP_ALL) >= 0) {
this.selectedAttributes =
attributeName === CONST.SEARCH_BUTTON_GROUP_ALL ? [] : [attributeName];
} else if (this.selectedAttributes.indexOf(attributeName) >= 0) {
const index = this.selectedAttributes.indexOf(attributeName);
if (index >= 0) {
this.selectedAttributes.splice(index, 1);
}
// debugger;
// this.selectedAttributes = this.selectedAttributes.filter(
// a => a !== attributeName && a !== CONST.SEARCH_BUTTON_GROUP_ALL);
} else {
if (attributeName === CONST.SEARCH_BUTTON_GROUP_ALL) {
this.selectedAttributes = [CONST.SEARCH_BUTTON_GROUP_ALL];
} else {
this.selectedAttributes.push(attributeName);
}
}
}
}
Html Component
<hello name="{{ lame }}"></hello>
<p>
Start editing to see some magic happen :)
</p>
<div class="btn-toolbar">
<button
type="button"
class="btn btn-outline-info m-1"
(click)="searchButtonGroupClick('all')"
[ngClass]="{active: selectedAttributes.indexOf('all') >= 0}"
>
All
</button>
<div *ngFor="let attribute of searchButtonGroupAttributes">
<button
type="button"
class="btn btn-outline-info m-1"
(click)="searchButtonGroupClick(attribute.name)"
[ngClass]="{active: selectedAttributes.indexOf(attribute.name) >= 0}"
>
{{attribute.name}}
</button>
</div>
</div>
The desired outcome would be to be able to deselect and select buttons in groups and store their value. I have debugged the code and the functionality of adding and removing values to array is working however this is not reflecting in the dom as buttons are not being selected deselected properly
EDIT
Error was in html condition of ngClass should be [ngClass]="{active: selectedAttributes.indexOf(attribute.name) >= 0}
You just have a typo at the template where you add/remove class for selected button.
Here should be >= :
[ngClass]="{active: selectedAttributes.indexOf(attribute.name) >= 0}"
I select the currency in the parent component of Vue using bootstrap select:
<template>
...
<div class = "dropdown">
<button class="btn btn-secondary dropdown-toggle" type="button"> {{currency}} </ button>
<div class = "dropdown-menu">
<button class = "dropdown-item" # click = "currencyChange ('USD')"> USD </ button>
<button class = "dropdown-item" # click = "currencyChange ('EUR')"> EUR </ button>
</div>
</div>
...
<div class = "box">
<box v-bind: currency-name = 'currency' />
</div>
<template>
<script>
...
data () {
return {
currency: 'USD'
}
},
components: {
box: component
},
methods: {
currencyChange (currency) {
this.currency = currency;
}
}
...
</script>
In the child component "box" I get data from server via the axios.get call and render it:
<script>
...
props: ['currencyName'],
data () {
return {
boxData: {},
}
},
created () {
axios.get (URL + this.currencyName)
.then (response => {
this.Data = response.data;
})
.catch (e => {
this.errors.push (e)
})
},
...
</script>
The problem is that if the EUR currency is selected, I never send new query to the server and data in the box component remains the same, for the 'USD' currency except currencyChange variable. If rename "created" hook to "updated" in component, everything starts to work as it should, with one exception - there are constant calls to the server.
How can I fix this wrong behaviour into a single call to the server only after dropdown click?
You can use the watch property to achieve this.
<script>
...
props: ['currencyName'],
data () {
return {
currentCurrency: this.currencyName,
boxData: {},
}
},
watch: {
currentCurrency() {
this.getData();
}
},
methods: {
getData() {
axios.get (URL + this.currentCurrency)
.then (response => {
this.Data = response.data;
})
.catch (e => {
this.errors.push (e)
})
},
}
...
</script>
Take a look at https://v2.vuejs.org/v2/guide/computed.html#Watchers
EDIT
As D F suggest you can add immediate: true, to your watcher to trigger it at the component initialization
StackBlitz
Here is my FormArray (variants):
this.productGroup = this.fb.group({
name: '',
variants: this.fb.array([
this.fb.group({
type: '',
options: ''
})
])
})
I'm using MatChips to store a string array. This array needs to be passed to options:
<div formArrayName="variants" *ngFor="let item of productGroup.controls['variants'].controls; let i = index;">
<div [formGroupName]="i">
<div class="row">
<mat-form-field class="col-12">
<input formControlName="type">
</mat-form-field>
</div>
<div class="row">
<mat-form-field class="col-12">
<mat-chip-list #chipList>
<mat-chip *ngFor="let opt of typesOptions" [selectable]="true"
[removable]="true" (removed)="removeOpt(opt)">
{{opt}}
<mat-icon matChipRemove>cancel</mat-icon>
</mat-chip>
<input placeholder="Conjunto de opções deste Tipo"
formControlName="options"
[matChipInputFor]="chipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
[matChipInputAddOnBlur]="true"
(matChipInputTokenEnd)="addOpt($event)">
</mat-chip-list>
</mat-form-field>
</div>
</div>
<div class="row">
Add Variants
Remove Variants
</div>
</div>
Here is the methods:
// Dynamic Methods
addItem(): void {
this.variantsArray = this.productGroup.get('variants') as FormArray;
this.variantsArray.push(this.fb.group({
type: '',
options: ''
}));
}
removeItem(index: number) {
this.variantsArray.removeAt(index);
}
// MatChip Methods
addOpt(item: number, event: MatChipInputEvent): void {
const input = event.input;
const value = event.value;
// Add our fruit
if ((value || '').trim()) {
this.typesOptions.push(value.trim());
}
// Reset the input value
if (input) {
input.value = '';
}
}
removeOpt(opt: string): void {
const index = this.typesOptions.indexOf(opt);
if (index >= 0) {
this.typesOptions.splice(index, 1);
}
I'm successfully adding dynamic fields to my variants formArray. However MatChipList is the same for every dynamic field. I need to make MatChipList dynamic as well. Is there a way to achieve this? Like changing <mat-chip-list #chipList+i> or something like that.
EDIT: StackBlitz
I'm not sure the dom reference variable #chiplist is the issue. It looks like the matChipList is backed by the typesOptions array, but you only have one array. So every time you add a matChipList component it is still being backed by the same array as all the others. You need to have an array of typesOptions, an array of arrays. Then when you addItem, you also push a new sub array to typesOptions (and similarly remove one for removeItem).
I haven't coded this up, just a suggestion from looking at the code.
Edit - coded up a solution based on James's stackblitz.
https://stackblitz.com/edit/angular-3od6rd-jkidxf
Note I haven't looked in detail at how the delete variant holds together, ideally I'd probably like to use a key/value pair to track the variant options using the dom input element id as the key (which is in the MatChipInputEvent), instead of relying on the outer loop index.
Some code from the stackblitz:
export class ChipsOverviewExample {
productGroup: FormGroup;
variantsArray: FormArray;
typesOptionsArray: string[][] = [];
readonly separatorKeysCodes: number[] = [ENTER, COMMA];
constructor(private fb: FormBuilder) {}
ngOnInit() {
this.productGroup = this.fb.group({
name: '',
variants: this.fb.array([
this.fb.group({
type: '',
options: ''
})
]),
});
this.typesOptionsArray.push([]);
}
saveProduct(form: FormGroup) {
console.log(form);
}
// Add new item to FormArray
addItem(): void {
this.variantsArray = this.productGroup.get('variants') as FormArray;
this.variantsArray.push(this.fb.group({
type: '',
options: ''
}));
this.typesOptionsArray.push([]);
}
removeItem(index: number) {
this.variantsArray.removeAt(index);
}
addOpt(event: MatChipInputEvent, index: number): void {
const input = event.input;
const value = event.value;
// Add our fruit
if ((value || '').trim()) {
this.typesOptionsArray[index].push(value.trim());
}
// Reset the input value
if (input) {
input.value = '';
}
}
removeOpt(opt: string, index: number): void {
const optIndex = this.typesOptionsArray[index].indexOf(opt);
if (optIndex >= 0) {
this.typesOptionsArray[index].splice(optIndex, 1);
}
}
}
try to make the formGroup as a new component and input formGroup to it(not formGroupName).
<div formArrayName="variants" *ngFor="let item of productGroup.controls['variants'].controls; let i = index;">
<variant [varientGroup]="item"><varient>
<div class="row">
Add Variants
Remove Variants
</div>
</div>
varient component.html
<div [formGroup]="varientGroup">
<div class="row">
<mat-form-field class="col-12">
<input formControlName="type">
</mat-form-field>
</div>
<div class="row">
<mat-form-field class="col-12">
<mat-chip-list #chipList>
<mat-chip *ngFor="let opt of typesOptions" [selectable]="true"
[removable]="true" (removed)="removeOpt(opt)">
{{opt}}
<mat-icon matChipRemove>cancel</mat-icon>
</mat-chip>
<input placeholder="Conjunto de opções deste Tipo"
[matChipInputFor]="chipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
[matChipInputAddOnBlur]="true"
(matChipInputTokenEnd)="addOpt($event)">
</mat-chip-list>
</mat-form-field>
</div>
</div>
in varient.component.ts
#Input()varientGroup: FormGroup