Nested form array in angular - javascript

I am making angular application with reactive form, where i have made a nested form array which will get nested on button click.
A clean working example https://stackblitz.com/edit/angular-thhczx , it has static inputs and hence on click over Add new template, it will add a another nested part and for Add new property, it will generate another property array..
So you had got the above working example concept right??
I would like to have the same json but not with add button and with dropdown.
Html of dropdown:
<select multiple [(ngModel)]="selectItems" (change)="changeEvent($event)">
<option *ngFor="let template of templates" [value]="template.key">{{template.value}}</option>
</select>
{{selectItems|json}}
<form [formGroup]="form">
<div *ngFor="let item of array">
{{item.value}} is the parent
<div *ngFor="let child of item.templateChild">
{{child.property_name}}
<input type="text" [value]="child.property_name">
</div>
<br><br><br>
</div>
</form>
<br><br><br>
{{form.value|json}}
Templates array: which gives value for dropdown
templates = [
{
key: 1, value: "Template one",
templateOneChild: [
{ property_name: "Property one" },
{ property_name: "Property two" }
]
},
{
key: 2, value: "Template two",
templateTwoChild: [
{ property_name: "Property three" },
{ property_name: "Property four" },
{ property_name: "Property five" }
]
},
{
key: 3, value: "Template three",
templateThreeChild: [
{ property_name: "Property six" },
{ property_name: "Property seven" }
]
}
]
Also made a stackblitz link for the above https://stackblitz.com/edit/angular-1sg5cv
Here if i select the option template one and template two (as the selectbox is multi select) from the dropdown then i am expecting the output as,
"template_details" : [
{ "template_name": "Template One",
"template_data" : [{"property_one": "", "property_two":""}]
},
{ "template_name": "Template Two",
"template_data" : [{"property_three": "", "property_four":"",
"property_five":""}]
}
]
If you click over the two options of template one and two you can see that you will get two and three input boxes respectively...
I wish to generate the input boxes with property names automatically under each template on selection of dropdown values..
So in simple need to convert dropdown demo as like the static inputs with add button with the same nested json structure.
I kindly request angular experts to help me in generation of input boxes based on property names for the selected template's..
I did my level best in it unable to get the solution please help me to form nested array json on based on selection of dropdown..

#Undefined, you need two different jobs
Create a formGroup
Display inputs that manage the formGroup
the first part is the easer. Go step by step, if you select template one, you need some like
this.fb.group({
template_name:"template one",
template_data:this.fb.array([
this.fb.group({
property_one:'',
property_two:''
})
])
})
but you want to do the things dinamically, so, make a function that receive an object and return a FormGroup. As you only need the "value" of the template and the childs, your function can be like
createFormGroup(value:string,children:any[]):FormGroup
{
/*e.g. for template one, you send
value: "Template one",
children: [
{ property_name: "Property one" },
{ property_name: "Property two" }
]
*/
let controls:FormGroup[]=children.map(
(x:any)=>this.fb.group({
[x.property_name]:''
})
)
return this.fb.group({
template_name:value,
template_data:this.fb.array(controls)
})
}
So yet we can create a formGroup for the differents templates and join in a FormArray
changeEvent(e) {
let arrayControls:FormGroup[] = [];
//in this.selectItems we have, e.g. [1,3]
for (let select of this.selectItems) {
//search the template, select will be e.g. 1,3
let template:any=this.templates.find(x=>x.key==select);
switch (+select) {
case 1:
arrayControls.push(this.createFormGroup(template.value,template.templateOneChild));
break;
case 2:
arrayControls.push(this.createFormGroup(template.value,template.templateTwoChild));
break;
case 3:
arrayControls.push(this.createFormGroup(template.value,template.templateThreeChild));
break;
}
}
this.form=this.fb.group({
template_details:this.fb.array(arrayControls);
})
}
See that if all ours children of templates was under a property "children" (not templateOneChild for the first, templateTwoChild for the seconds...) our function becomes in
changeEvent(e) {
let arrayControls:FormGroup[] = [];
//in this.selectItems we have, e.g. [1,3]
for (let select of this.selectItems) {
//search the template, select will be e.g. 1,3
let template:any=this.templates.find(x=>x.key==select);
arrayControls.push(this.createFormGroup(template.value,template.children));
}
this.form=this.fb.group({
template_details:this.array(arrayControls);
})
}
Well you have the "form" created, now is time to show it. The form is like
<div *ngIf="form">
<form [formGroup]="form">
<div formArrayName="template_details">
<div *ngFor="let item of details.controls;let i=index" [formGroupName]="i">
<input formControlName="template_name">
<div formArrayName="template_data">
<div *ngFor="let child of item.get('template_data').controls;let j=index" [formGroupName]="j">
<input formControlName="??????????">
</div>
</div>
</div>
</div>
</form>
</div>
Yes, we have a problem, we don't know the "formControlName" of the inner formArray. One solution is have a variable "controlsName" that will be an array of array, so, if e.g. we choose 1 and 3 template our controlsName was like
controlsName=[
["property_one","property_two"],
["property_six",property_seven"]
]
Well, again make a function that return an array of strings with the names of the properties. it's a simply version of our createFormGroup, receive "children" and return an array of strings.
getControlNames(children:any[]):string[]
{
let controlNames:string[]=children.map(x=>x.property_name);
return controlNames;
}
Well, in changeEvent we call to this function after call to createFormGroup
changeEvent(e) {
let arrayControls:FormGroup[] = [];
let controlsName:string[] = []; //<--add this line
for (let select of this.selectItems) {
let template:any=this.templates.find(x=>x.key==select);
switch (+select) {
case 1:
arrayControls.push(this.createFormGroup(template.value,template.templateOneChild));
controlsName.push(this.getControlNames(template.templateOneChild)); //<--and this
break;
... idem with case 2 and case 3...
}
}
this.controlsName=controlsName; //<--give value to name first
//then create the form
this.form=this.fb.group({
template_details:this.fb.array(arrayControls);
})
After this, replace the < input formControlName="??????????" > by
<input [formControlName]="controlsName[i][j]">
See that we use [formControlName] (not formControlName) because is an evaluated expression.
See the stackblitz here

I am not sure about your question.You want to dynamically add controls using json.
Reference link : https://angular.io/guide/dynamic-form
Working example : https://stackblitz.com/edit/angular-srpk3w
Replace your files with the below code :
app.component.html
<select multiple [(ngModel)]="selectItems" (change)="changeEvent($event)">
<option *ngFor="let template of templates" [value]="template.key">{{template.value}}</option>
</select>
{{selectItems|json}}
<div *ngIf="form">
<form [formGroup]="form">
<div *ngFor="let item of array">
{{item.value}} is the parent
<div *ngFor="let child of item.templateChild; index as i">
{{child.property_name}}
<input type="text" formControlName="{{child.property_name.split(' ').join('_')}}" [value]="child.property_name" >
</div>
<br><br><br>
</div>
</form>
</div>
<br><br><br>
{{form.value|json}}
app.component.ts
import { Component } from '#angular/core';
import { FormControl, FormGroup, Validators } from '#angular/forms';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent {
array: any[] = [];
selectItems: any;
form: FormGroup;
templates = [
{
key: 1, value: "Template one",
templateChild: [
{ property_name: "Property one" },
{ property_name: "Property two" }
]
},
{
key: 2, value: "Template two",
templateChild: [
{ property_name: "Property three" },
{ property_name: "Property four" },
{ property_name: "Property five" }
]
},
{
key: 3, value: "Template three",
templateChild: [
{ property_name: "Property six" },
{ property_name: "Property seven" }
]
}
]
changeEvent(e) {
this.array = [];
for (let select of this.selectItems) {
this.array.push(this.templates[select-1])
this.form=this.getFormValue(this.array);
}
}
getFormValue(array){
let group: any = {};
array.forEach(r=>{
r.templateChild.forEach((t,index)=>{
group[t.property_name.replace(/ /g, "_")]= new FormControl(t.property_name);
})
})
return new FormGroup(group);;
}
}

Related

Vue allow only one element in array be true

I'm creating multiple input fields with checkboxes in vue and I want that only one can be true. So if the user clicks on one the others should be false so that only the last clicked checkbox is true.
My code is like that:
new Vue({
el: "#app",
data: {
selected: null,
options: [
{"id": 1, "title": "One", "value": false},
{"id": 2, "title": "Two", "value": false },
{"id": 3, "title": "Three", "value": false},
]
},
watch: {
selected(selected) {
this.options.forEach((option, index) => {
option.id == selected ? option.value = true : option.value = false;
});
}
}
Unfortunately my watcher isn't working properly. I would be really glad if somebody can show me how to correct it. I want that always the last true element is the only true element and the watches sets all other elements in options to false.
If i understood your requirements correctly you can still do it with radio buttons. You can specify the value to be used inside the selected variable, as described here: https://v2.vuejs.org/v2/guide/forms.html#Radio-1. This means that you can set up a watcher and then mutate the options list accordingly:
selected: function (newVal) {
this.options.forEach(option => {
if (option.id === newVal) option.value = true
else option.value = false
})
console.log(this.options)
}
Here is a sandbox to see it in action:
https://codesandbox.io/s/heuristic-goldberg-lilsw
Update: Just saw that you want to use the </b-switch> from buefy. You can still do something similar by calling a function from the input event which then mutates the options list according to the just changed element. Something like this:
<div v-for="(option,index) in options" :key="index">
<div class="box">
<div class="field">
<b-switch v-model="option.value" #input="(modelValue) => onSwitchChanged(modelValue, option.id)">
{{ option.title }}
</b-switch>
<label :for="index">{{ option.title }}</label>
</div>
</div>
</div>
function onSwitchChanged(modelValue, id) {
if (!modelValue) return
this.options.forEach(option => {
if (option.id === id) option.value = true
else option.value = false
})
}
If you want that Only One will be selected then you have to use radio button. Checkbox has options to select all But One by One.
Without watch you can use methods. Pass index to the method.
<input
type="checkbox"
:id="index"
:value="option.id"
#click="selectAnOption(index)"
>
Method:
methods: {
selectAnOption(index) {
this.options[index].value = true
}
}
Full Code here: https://jsfiddle.net/8ktdp9ew/

How to get select option in Vue.js

I am try to use Vue.js. I write javascript code like...
new Vue({
el: '#app',
data: {
classes: []
},
created: function () {
var vm = this
// Fetch API
fetch(xxx.json)
.then(function (response) {
return response.json();
})
.then(function (data) {
vm.classes = data.classes;
})
}
});
The program will fetch a JSON file first.The JSON format like
{
"classes": [
{
"name": "A",
"students": [
{
"name": "Eric",
"fruit": [
"apple",
"banana",
"orange"
]
},
{
"name": "Eickson",
"fruit": [
"banana",
"orange"
]
}
]
},
{
"name": "B",
"students": [
{
"name": "Ethan",
"fruit": [
"banana",
"apple"
]
}
]
}
]
}
Then put the JSON data into the data . Then I want the user can pick the items in each class. And use HTML to draw each class and each student
Imgur
HTML Code like...
<div class="row">
<div class="col-3" v-for="class in classes">
Class name: {{ class.name}}</br>
<div class="form-group row" v-for="student in cless.students">
<label class="col-form-label col-sm-2">{{ student.name }}</label>
<div class="col-sm-10">
<select class="form-control" :name="class.name+'-'+student.name">
<option></option>
<option v-for="fruit in class.fruit">{{fruit}}</option>
</select>
</div>
</div>
</div>
</div>
<button type="submit" " class="btn btn-primary">Submit</button>
I want to use the submit button to get all the selected option.
I tried to put a function in method. And button add #click="submitFunc() . But I have no idea to do it...
Please help me how to implement it. Thanks.
There are a couple immediate ways.
First, you can take the selected item from the select element by simple adding a method and taking the option natively.
Create a method submitFunc in your component's method options, in that method, query the select element.
new Vue({
//...
methods: {
submitFunc: function(){
let select = this.$el.querySelector('#mySelect'); //add an id for ease
let selectedOption = select.options[select.selectedIndex];
console.log(selectedOption); //will output the element, you can get the value at this point.
//do something else
}
}
})
Secondly, thanks to #Kokodoko, you can use the Vue's two-way data binding by declaring a selectedItem property in the data options, attaching it in the v-model attribute of the select element, and then accessing it via submitFunc method.
new Vue({
el: '#app',
data: {
classes: [],
selectedItem: null //this will be your selected option
},
methods:{
submitFunc: function(){
const selectedItem = this.selectedItem;
//do something with selectedItem
}
}
});
In the template
<select v-model="selectedItem">
<option></option>
<option v-for="fruit in class.fruit">{{fruit}}</option>
</select>
<button v-on:click="submitFunc">Submit</button>

Array of Objects conditional style in textarea. Angular 6

I have an array of objects like this:
list =[
{
name:"name1",
value:true
} {
name:"name2",
value:false
} {
name:"name3",
value:true
} {
name:"name4",
value:false
}
]
What I want to do is to show inside a text area all objects names and if object.value is false underline or bold the line. And then to be able to write inside the text area to remove the underlined elements.
What I tried was:
<textarea *ngFor="let item of list" [ngClass]="{cssClass: item.value==false}">
{{item.name}}
</textarea>
-The problem here is that it shows an empty text area for each object
AND
<div *ngFor="let item of list" [ngClass]="{cssClass: item.value==false}">
<textarea>
{{item.name}}
</textarea>
</div>
The problem here is that it creates a textarea per line, where the line is inside.
You have string value not the the boolean type. So you should use string value 'true' or 'false' not true and false.
<textarea *ngFor="let item of list" [ngClass]="{cssClass: item.value=='false'}">
OR
If you have option to modify the object then change the value type as -
list =[
{
name:"name1",
value:true
} {
name:"name2",
value:false
} {
name:"name3",
value:true
} {
name:"name4",
value:false
}
]

Create inputs as array in angular form

Tried my level best please help me out..
I am making an angular dynamic form with a form splitting into total of three parts in which the two parts were made already. Now I am making part three which will be generated by selecting an option from dropdown...
Even those part also is done...
But I am unable to make a form array in it... As I am new in angular please help me.
HTML:
Form part 3 will be here which will be array
<select multiple (change)="changeEvent($event)">
<option *ngFor="let opt of persons" [value]="opt.key">{{opt.value}}</option>
</select>
<div *ngFor="let item of array">
{{item.value}} is the parent
<div *ngFor="let child of item.templateChild">
{{child.property_name}}
<div *ngFor="let partThree of questionsParthree">
<ng-container>
<app-question [question]="partThree" [form]="formPartThree"></app-question>
</ng-container>
</div>
</div>
</div>
Select Box change event:
changeEvent(e) {
if (e.target.value == 1) {
this.array = [];
this.array.push(
{
key: 1, value: "Template one",
templateChild: [
{ property_name: "Property one" },
{ property_name: "Property two" }
]
}
);
let propertiesArray = [];
this.array.forEach(element => {
element.templateChild.forEach(data => {
propertiesArray.push(
{
key: data.property_name,
label: data.property_name,
"elementType": "textbox",
"type": "text"
}
)
});
});
this.questionsPartThree = this.service.getQuestions(propertiesArray);
this.formPartThree = this.qcs.toFormGroup(this.questionsPartThree);
this.formJoin = new FormGroup({ form1: this.form, form2: this.formPartTwo, form3: this.formPartThree });
}
}
Continuation like part 1 and 2..
I have posted the code related to creating the form array alone..
Updated Stackblitz https://stackblitz.com/edit/angular-x4a5b6-mnyifs
Here if we select any option then you will get the input boxes with values..
I would like to create array with it like,
If we select the first option Template One, Output expected is exactly as like
"templateChild" : [
{"property_one": "", "property_two":""}
]
So the final output of whole form going to be if i select Template One and also Template Two from select box (as select box is multi select),
{
"form1": {
"project_name": "",
"project_desc": ""
},
"form2": {
"property_one": "",
"property_two": ""
},
"template_details" : [
{ "template_name": "Template One",
"templateChild" : [{"property_one": "", "property_two":""}]
},
{ "template_name": "Template Two",
"templateChild" : [{"property_three": "", "property_four":"",
"property_five":""}]
}
]
}
Have a work around in the demo i have provided and give me a better solution..
Kindly please help me to create a form as like the above when we select an option from dropdown.. If i am wrong in my approach also please correct me..
If i finish this third part then everything will get alright any angular technical expert please help me..
Taking too long please help me out..
You can dynamically change an AbstractController inside a FormGroup using the setControl() method.
Add an empty form3 part for the moment
this.form3 = new FormGroup({});
this.formJoin = new FormGroup({ form1: this.form, form2: this.formPartTwo, form3: this.form3 })
When selecting an item, generate a new FormGroup according the form you create.
if (e.target.value == 1) {
this.array = [];
this.form3 = new FormGroup({'Property one': new FormControl('insert whatever you want')});
this.formJoin.setControl('form3', this.form3);
You should be able to do something what that start!

how to datalink the index with the json object?

I have the following JSON object which maintain the sequence in it.
var sample={
"sample": [
{
"example": [
{
"sequence": 1,
},
{
"sequence":2
},
{
"sequence":3
}
]
},
{
"example": [
{
"sequence": 1,
}
]
}
]
};
$.templates("testingTemplate", "#testingSection");
var html=$.link.testingTemplate("#htmlHolder", sample);
$("#insert").click(function(){
var childIndexVal=parseInt($("#childIndex").val());
var x= {
"sequence": childIndexVal+1,
"xxx":"yyy"
};
var parentIndexVal=parseInt($("#parentIndex").val());
$.observable(sample.sample[parentIndexVal].example).insert(childIndexVal,x);
console.log(sample);
});
.parentHolder
{
border:1px solid red;
padding:5px;
margin:5px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.0/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jsviews/0.9.80/jsviews.js"></script>
<script id="testingSection" type="text/x-jsrender">
{{if sample && sample.length}}
{^{for sample }}
<div class="parentHolder">
{^{for example }}
<div><span data-link="#index+1" ></span>
<input type="text" value="{{:sequence}}" id="sequence" data-link="#index+1"></div>
{{/for}}
</div>
{{/for}}
{{/if}}
</script>
<div id="htmlHolder">
</div>
<div class="messages">
</div>
<span>Parent Index:</span><input type="text" id="parentIndex"/>
<span>Child Index:</span><input type="text" id="childIndex"/>
<button id="insert" >Insert</button>
so whenever I insert the object it should update the squence accodring to the index.
In the given example, if I am inserting an object into the 1st index of example in sample[0].(consider example array index starts from zero).
So when I am inserting one object into first parameter, the sequence in the remaining object should updated according to the index.
How can I achieve it.
give parent Index as 0 and child index as 1.
expected output,
Note: extra "xxx":"yyy" for differntiation purpose.
var sample={
"sample": [
{
"example": [
{
"sequence": 1,
},
{
"sequence": 2,
"xxx":"yyy"
}
{
"sequence":3
},
{
"sequence":4
}
]
},
{
"example": [
{
"sequence": 1,
}
]
}
]
};
Update: : tried with linkTo also.
<input type="text" data-link="linkTo=sequence #index+1" ></div>
Still not getting the expected output.
Thanks in advance.
Neither JsRender nor JsViews will modify the JSON data that they are rendering. This is by design: there are no side-effects on the data...
On the other hand, you can write code to create side-effects on the data, or you can use two-way binding so a user can modify data values. But two-way binding will only change the targetted data value, not other values elsewhere, and will do so only when the user triggers a change event on that <input> for example.
In your case you want any observable changes to an examples array to trigger changes to all examples in the array such as to ensure the example.sequence value is always equal to the index+1 for each example. In that case you have to write code to do that.
One way you can achieve that is to add the following code using observeAll:
$.observable(sample).observeAll(function(ev, eventArgs) {
if (ev.type==="arrayChange" && ev.data.observeAll.path().slice(-7)==="example") {
$.each(ev.currentTarget, function(i, item) {
$.observable(item).setProperty("sequence", i+1);
})
}
});
That will ensure that the sequence value stays in sync not only for insert, but also when items are removed or for changes in the the order of items, etc.

Categories

Resources