Angular 4 Create Dynamic formArray inside array using reactive forms - forms

Here, we are creating dynamically form array's inside array.
Below is the sample structure of expected result given below.
{
"optionsRadios": null,
"Package_Title": null,
"HotelData": [
{
"Htitle": "",
"HDescription": "",
"hotelStar": "",
"RoomData": [
{
"Hotel_Room_Type": ""
},
{
"Hotel_Room_Type": ""
}
]
}
]
}
I want to create HotelData Dynamically, within that HotelData array i want to create RoomData array fields also dynamically.
I created HotelData fields by the following codes:
export class AddPackageComponent implements OnInit {
ngOnInit() {
this.invoiceForm = this._formBuild.group({
Package_Title: [],
HotelData: this._formBuild.array([this.addRows()])
})
}
addRows() {
return this._formBuild.group({
Htitle: [''],
HDescription: [''],
hotelStar: ['']
});
}
addHotel() {
const control: FormArray = this.invoiceForm.get(`HotelData`) as FormArray;
control.push(this.addRows());
}
}

You are on the right track, we just need to add some more code...
addRows need the form array RoomData, and here we also initially push an empty form group of room. If you don't want that, modify it.
addRows() {
let group = this._formBuild.group({
...
RoomData: this._formBuild.array([])
});
// push formgroup to array initially
this.addRoom(group.controls.RoomData)
return group;
}
addRoom looks like this:
addRoom(hotel:any) {
let group = this._formBuild.group({
Hotel_Room_Type: ['']
})
hotel.push(group)
}
addRoom is also the method we are calling from template when we want to add a new room to a hotel. Remember to pass the current hotel as parameter from template.
As for adding a new hotel, your addHotel stays the way you have it now.
Then over to your template, the relevant part should look something like this:
<div formArrayName="HotelData">
<div *ngFor="let hotel of invoiceForm.get('HotelData').controls; let i = index" [formGroupName]="i" >
<!-- form controls here -->
<button (click)="addRoom(hotel.get('RoomData'))">Add Room</button>
<div formArrayName="RoomData">
<div *ngFor="let room of hotel.get('RoomData').controls; let j = index" [formGroupName]="j">
<!-- form controls here -->
</div>
</div>
</div>
</div>
Finally, here's a Demo: http://plnkr.co/edit/7tcLcnzALew3oKGenjwK?p=preview

Related

What is the best way to post form with multiple components using Vue js

as I'm on my Vue spree (started recently but so far I'm really enjoying learning this framework) couple of questions rised up. One of which is how to post form from multiple components. So before I continue forward I wanted to ask you what are you thinking about this way of structuring and point me in right direction if I'm wrong.
Here it goes.
I'm working on a SPA project using ASP.NET CORE 2.1 and Vue JS Template (with webpack)(https://github.com/MarkPieszak/aspnetcore-Vue-starter) and my project is structured in several containers, something like this:
In my app-root i registered several containers
<template>
<div id="app" class="container">
<app-first-container></app-first-container>
<app-second-container></app-second-container>
<!--<app-third-container></app-third-container>-->
<app-calculate-container></app-calculate-container>
<app-result-container></app-result-container>
</div>
</template>
<script>
// imported templates
import firstContainer from './first-container'
import secondContainer from './second-container'
import calculateContainer from './calculateButton-container'
//import thirdContainer from './third-container'
import resultContainer from './result-container'
export default {
components: {
'app-first-container': firstContainer,
'app-second-container': secondContainer,
// 'app-third-container': thirdContainer,
'app-calculate-container': calculateContainer,
'app-result-container': resultContainer
}
}
</script>
In my first container I'm having several dropdowns and two input fields with my script file where I'm fetching data from API and filling dropdowns and input fields with fetched data.
Something like this ( entered some dummy code for demonstration)
<template>
<div>
<h1>Crops table</h1>
<p>This component demonstrates fetching data from the server. {{dataMessage}}</p>
<div class="form-row">
<div class="form-group col-md-6">
<label for="exampleFormControlSelect1" class="col-form-label-sm font-weight-bold">1. Some text</label>
<select class="form-control" id="exampleFormControlSelect1" v-model="pickedCropType" #change="getCropsByType()">
<option v-for="(cropType, index) in cropTypes" :key="index" :value="cropType.id" :data-imagesrc="cropType.imgPath">{{ cropType.name }}</option>
</select>
</div>
<div class="form-group col-md-6">
<label for="exampleFormControlSelect2" class="col-form-label-sm font-weight-bold">2. Some text</label>
<select class="form-control" id="exampleFormControlSelect2">
<option v-for="(crop, index) in cropSelectList" :key="index" :value="crop.id">{{ crop.name }}</option>
</select>
</div>
</div>
</div>
</template>
<script>
import { mapActions, mapState } from 'vuex'
export default {
data() {
return {
cropTypes: null,
cropSelectList: null,
crops: null,
pickedCropType: null,
}
},
methods: {
loadPage: async function () {
try {
//Get crop types and create a new array with crop types with an added imgPath property
var cropTypesFinal = [];
let responseCropTypes = await this.$http.get(`http://localhost:8006/api/someData`);
responseCropTypes.data.data.forEach(function (element) {
cropTypesFinal.push(tmpType);
});
} catch (err) {
window.alert(err)
console.log(err)
}
},
getCropsByType: async function () {
//Get crops by crop type
let responseCrops = await this.$http.get(`http://localhost:8006/api/crop/Type/${this.pickedCropType}`);
var responseCropsData = responseCrops.data.data;
this.cropSelectList = responseCropsData;
}
},
async created() {
this.loadPage()
}
}
</script>
And in my second container I have different dropdowns and different input fields with different scripts etc.
So, my questions are:
1.) I'm having required data form field in first container and in second container I'm having additional data and my submit button is separated in third container (app-result-container). So, is this proper and logical way of structuring containers if not can you point me in right direction?
2.) Is it smart to input script tag in every container where I'm processing/fetching/submitting some data for that particular container? Should I put scripts tag in separated file and keep structure clean, separating html from js file.
Example:
import { something } from 'something'
export default {
data () {
return {
someData: 'Hello'
}
},
methods: {
consoleLogData: function (event) {
Console.log(this.someData)
}
}
}
3.) Can I send input values from one container to another (In my particular case from first and second container to app-calculate-container(third container))?
How to on submit return results container with calculated imported values
If you want components to communicate or share data with one another, you will need to either emit an event from one component up to the parent and pass it down via props, or use some kind of state management model, like Vuex, where each of your components can listen to the store.
Take a look at this code sandbox: https://codesandbox.io/s/8144oy7xy2
App.vue
<template>
<div id="app">
<child-input #input="updateName" />
<child-output :value="name" />
</div>
</template>
<script>
import ChildInput from "#/components/ChildInput.vue";
import ChildOutput from "#/components/ChildOutput.vue";
export default {
name: "App",
components: {
ChildInput,
ChildOutput
},
data() {
return {
name: ""
};
},
methods: {
updateName(e) {
this.name = e.target.value;
}
}
};
</script>
ChildInput.vue
<template>
<input type="text" #input="changeHandler">
</template>
<script>
export default {
name: "ChildInput",
methods: {
changeHandler(e) {
this.$emit("input", e);
}
}
};
</script>
ChildOutput.vue
<template>
<p>{{ value }}</p>
</template>
<script>
export default {
name: "ChildOutput",
props: {
value: {
type: String,
default: ""
}
}
};
</script>
What's going on?
The ChildInput component is a text field and on every change inside it, fires an event (emits using this.$emit() and passes the whole event up).
When this fires, App is listening to the change, which fires a method that updates the name data property.
Because name is a reactive data property and is being passed down as a prop to the ChildOutput component, the screen re-renders and is updated with the text written.
Neither ChildInput nor ChildOutput knows about one another. It's the parent that listens to the event passed to it, then passes the new prop down.
This way of working is fine and simple to understand, but I would strongly recommend looking at Vuex, as this method can get messy and complicated when you go beyond trivial tasks.

Angular2 default 'selected' select option not being set

I am having an issue getting select boxes on my Angular2 application to show a default option '--please select--' on page load. I have managed to get this working before but I cannot seem to get this working in this particular instance. I'll show my code then explanations as I show it.
Here is my relevant controller code:
import {Component} from "#angular/core";
import {ProductService} from "../../services/product.service";
import {Subscription} from "rxjs";
import {ActivatedRoute} from "#angular/router";
#Component({
selector : 'product',
moduleId : module.id,
templateUrl : '/app/views/products/product-view.html'
})
export class ProductComponent {
private id:number;
private _subscription: Subscription;
public product;
private price;
private quantity = 0;
constructor(
private _productService: ProductService,
private _activatedRoute: ActivatedRoute
) {
}
getProduct(productId: number) {
this._productService.getProduct(productId)
.subscribe((response) => {
response.success.product.options.forEach((option) => {
this[option.name] = {
name: '-- please select --',
product_option_value_id: 0,
price: 0,
price_prefix: '+'
};
option.product_option_value.unshift({
name: '-- please select --',
product_option_value_id: 0,
price: 0,
price_prefix: '+'
});
});
this.product = response.success.product;
this.generatePrice();
});
}
changeOption(optionValueId, option) {
if(optionValueId != 0) {
let selectedOptionValue = option.product_option_value.filter((option) => {
return option.product_option_value_id == optionValueId;
});
this[option.name] = selectedOptionValue[0];
} else {
this[option.name] = {
name: '-- please select --',
product_option_value_id: 0,
price: 0,
price_prefix: '+'
};
}
this.generatePrice();
}
.....
Here I am getting back information about a product which includes 'options' in a form of an array. This array of objects is iterated over to create the select boxes in the view code which will come later. I add a default '-- please select --' object for each option and put it to the front of the array using unshift. I then also set the controller value for this in the line:
this[option.name] = {
name: '-- please select --',
product_option_value_id: 0,
price: 0,
rice_prefix: '+'
};
The relevant view code is as follows:
<div class='product-options'>
<div class='option' *ngFor='let option of product.options; let i = index'>
<p class='option-name' [innerHTML]='option.name'></p>
<select name='option.name' [ngModel]='option.name' (ngModelChange)='changeOption($event, option)' required>
<option *ngFor='let productOptionValue of option.product_option_value; let j = index;' [value]='productOptionValue.product_option_value_id'>{{ productOptionValue.name }}</option>
</select>
</div>
<div class='price' ngDefaultControl [(ngModel)]="price">{{ price | currency:'GBP':true:'1.2-2' }}</div>
<div class='add-to-basket-wrap'>
<button class='add-to-basket'>add to basket</button>
<button class='increment' (click)="changeQuantity('down')">-</button>
<input type='text' name='quantity-to-add' [(ngModel)]="quantity" (click)='addToBasket()' />
<button class='increment'(click)="changeQuantity('up')">+</button>
</div>
</div>
Here I loop through the options then through the values for these options to generate the select boxes. I set the [ngModel] attribute for the select to the same as the one that was saved in my controller. I was under the impression that Angular2 would detect this binding, spot the value was the same as the controller value and then automatically set that as the 'selected' default option.
Can anyone see why this isn't working?
Thanks
Looks like your code is not working because your template and your class are not binding to the same properties.
In your component template, when you write this:
<div *ngFor="let option of product.options; let i = index">
<select [ngModel]="option.name">...</select>
</div>
... you're effectively binding each <select> to a class property named product.options[i].name.
On the other hand, in your component class, when you write this:
changeOption(optionValueId, option) {
this[option.name] = selectedOptionValue[0];
}
... you're writing to a class property named after whatever string is contained in option.name, e.g. foo.
As you can see, product.options[i].name and foo don't match. Even by changing foo to another string, you won't be able to access the property you want.
A few remarks/questions that might help:
It's a bit strange to store options inside "dynamic" class properties — this[option.name] = .... Why not store them in a dedicated this.options property that you can declare, type, and log out for debugging purposes: this.options[option.name] = ....
Why did you decide to use <select [ngModel]="..." (ngModelChange)="..."> vs the more compact <select [(ngModel)]="...">?
Any reason why you're using simple quotes on your HTML attributes, e.g. <div class='price'> vs <div class="price">? This is not the usual style.

Angular 2: Is there an easy way using FormBuilder with FormArray in ng2?

Situation
I'm working on a form where I want to list some mails having a checkbox besides the subject and a "check all" checkbox in the same column as the other checkboxes.
The form looks simply like this:
[ ] Check all
------------------------------------------
[ ] This is email subject #1
[ ] This is email subject #2
[ ] ...
When I select Check all all the below checkboxes should be selected and when I click again, all mails should be unselected.
The mails are coming dynamically into the component via an #Input and the list can change at any point of time.
So far so easy, nothing special. BUT it seems not so easy when using the FormBuilder in ng2 for that. Side note: I want to use the FormBuilder to test my code less end-to-end but more with unit tests.
Current code
Template
<form [formGroup]="form">
<div><input formControlName="toggleAll" type="checkbox"></div>
<div>
<ul formArrayName="checkMailList">
<li *ngFor="let mail of mails; let i=index">
<input type="checkbox" [formControlName]="i">
<div>mail.subject</div>
</li>
</ul>
</div>
</form>
Component
#Component({ ... })
export class MailListComponent implements OnChanges {
#Input() mails: Mail[];
private get checkMailList(): FormArray { return this.form.get('checkMailList') as FormArray; }
constructor(private fb: FormBuilder) {
this.form = this.fb.group({
checkMailList: this.fb.array([]);
});
}
ngOnChanges(changes: SimpleChanges) {
if (!changes['mails'] || !changes['mails'].currentValue) {
return;
}
// remove array first from form, as we will get
// mails again when anything updates
if (this.checkMailList.length > 0) {
this.form.removeControl('checkMailList');
this.form.addControl('checkMailList', this.fb.array([]));
}
this.mails.forEach(m => {
this.checkMailList.push(this.fb.control());
});
this.form
.valueChanges
.pluck('toggleAll')
.distinctUntilChanged()
.subscribe(selectAll => {
if (selectAll) {
this.checkMailList.setValue(this.mails.map(_ => true));
} else {
this.checkMailList.reset();
}
});
}
}
Problem
I think that there could occur a race condition/timing issue: I iterate over the mails array provided by #Input but I wire the checkMailList manually in the template to the corresponding index. I iterate over all mails whenever the #Input changes. I don't know if Angular first iterates over all mails in the template and then runs the ngOnChange method or vice versa. Can anyone can give me a profounded answer here?
Forms are the fundamental part of every WebApp. Am I doing it right? Any help would be appreciated.

How do I use React and forms to get an array of checked checkbox values?

I am trying to build a filter for my portfolio website. Checkboxes that let you pick a technology (react, redux, jquery etc.) to display a piece of work(s) that contain(s) that/those technologies. So every time the user clicks on a box, I want to add the value (JavaScript, Redux, React etc.) to an array that I use in another function to check against my portfolio pieces and filter out what isn't there.
I am finding this very difficult and I think it should be quite simple. Can someone point me in the right direction? Is there a way to simply have a function trigger (onChange callback?) that reads the checked/unchecked status of my form input elements and then updates my state array accordingly? Can I get the status of all the checkboxes simply in React? Do I need to have individual state of checked/unchecked for my checkboxes?
It seems that jQuery makes it pretty possible with selectors with:
$('input[type="checkbox"]:checked').each(function () {}
If you don't care about the order and you just want to append the items to the array as they appear we could definitely do exactly what you suggest in your question. On the change event of the checkbox check if the box is checked or or unchecked (event.target.checked returns true if checked or false if unchecked) and handle the array logic accordingly. this is a simple representation of how that could work:
import React, { Component } from 'react'
import { connect } from 'react-redux'
class Portfolio extends Component {
constructor() {
super()
// initialize your options array on your state
this.state = {
options: []
}
}
onChange(e) {
// current array of options
const options = this.state.options
let index
// check if the check box is checked or unchecked
if (e.target.checked) {
// add the numerical value of the checkbox to options array
options.push(+e.target.value)
} else {
// or remove the value from the unchecked checkbox from the array
index = options.indexOf(+e.target.value)
options.splice(index, 1)
}
// update the state with the new array of options
this.setState({ options: options })
}
render() {
return (
<main className='portfolio'>
<form>
<div className="input-group">
<label>cb1</label>
<input type="checkbox" value={1} onChange={this.onChange.bind(this)} />
</div>
<div className="input-group">
<label>cb2</label>
<input type="checkbox" value={2} onChange={this.onChange.bind(this)} />
</div>
<div className="input-group">
<label>cb3</label>
<input type="checkbox" value={3} onChange={this.onChange.bind(this)} />
</div>
</form>
<div className="selected-items">
{this.state.options.map(number =>
<p key={number}>item: {number}</p>
)}
</div>
</main>
)
}
}
if you DO care about order, if you can append numerical values to the array like I did in this example you could easily give your checkboxes sorted numerical values and you could sort the array before updating your state so it's always in a certain order regardless of the order they are checked.
onChange(e) {
// current array of options
const options = this.state.options
let index
// check if the check box is checked or unchecked
if (e.target.checked) {
// add the numerical value of the checkbox to options array
options.push(+e.target.value)
} else {
// or remove the value from the unchecked checkbox from the array
index = options.indexOf(+e.target.value)
options.splice(index, 1)
}
// sort the array
options.sort()
// update the state with the new array of options
this.setState({ options: options })
}
Here's how I'm doing it:
// util.js
import getPath from 'lodash/get';
import setIn from 'lodash/fp/set';
export function linkMultiCheck(name, value) {
return {
checked: getPath(this.state, name, []).includes(value),
onChange: ev => {
let values = getPath(this.state, name, []);
if(ev.target.checked) {
values = [...values, value];
} else {
values = values.filter(v => v !== value);
}
this.setState(setIn(name, values));
},
}
}
// form.js
<ul>
{options.branches.map(branch => (
<li key={branch.id} className="checkbox">
<label>
<input type="checkbox" name={this.id} {...this::linkMultiCheck('formData.branchIds',branch.id)}/>
{branch.id}
</label>
</li>
))}
</ul>
i.e., if a checkbox is checked, append it to the current array of values. If it's unchecked, filter it out.
I'm using lodash here so that we can set deeply nested state values using dot notation.

CQ5/AEM6/Sightly - Return custom type from Java Use-Api

Using JavaScript Use-Api I am able to create a custom object and return it to a html file. This feature allows me to create a list of custom objects, which can be used to create a menu or other complex list-like component.
Let's assume that I have following content structure:
/content
/project
/homepage
/contentpage1
/contentpage1.1
/contentpage1.2
/contentpage1.3 (hidden)
/contentpage2
/contentpage1.1 (hidden)
/contentpage1.2 (hidden)
/contentpage1.3 (hidden)
/contentpage3
/contentpage4
Menu should contains only first-level contentpages. Each menu item should have dropdown list with second-level contentpages, if they exist and are not hidden. I can do it in JavaScript with the following code:
"use strict";
use(function() {
function getMenuItems() {
var currentPageDepth = currentPage.getDepth();
var menuObjects = [];
if(currentPageDepth >= 3) {
var homePage = currentPage.getAbsoluteParent(2);
var list = homePage.listChildren();
while(list.hasNext()) {
var tempPage = list.next()
var customPageObject = createMenuItemObject(tempPage);
menuObjects.push(customPageObject);
}
}
return menuObjects;
}
function createMenuItemObject(page) {
// ...
// looking for any other properties of page or its children
// ...
return {page: page,
visibleChildrenExists: visibleChildrenExists(page)};
}
function visibleChildrenExists(page) {
var list = page.listChildren();
var visibleChildrenExists = false;
while(list.hasNext()) {
var subPage = list.next();
if(!subPage.isHideInNav()) {
visibleChildrenExists = true;
break;
}
}
return visibleChildrenExists;
}
return {
menuObjectsList: getMenuItems(),
};
}
HTML:
<headerComponent data-sly-use.headerComponentJS="headerComponent.js" data-sly-unwrap />
<menuItems data-sly-list.menuItem="${headerComponentJS.menuObjectsList}" data-sly-unwrap >
<li class='${menuItem.visibleChildrenExists ? "" : "direct"}' data-sly-test="${!menuItem.page.hideInNav}">
${menuItem.page.title}
<ul data-sly-test="${menuItem.visibleChildrenExists}" data-sly-list.submenuItem="${menuItem.page.listChildren}">
<li data-sly-test="${!submenuItem.hideInNav}">
${submenuItem.title}
</li>
</ul>
</li>
</menuItems>
Why do I want to use Java Use-Api? It's easier to operate on interfaces like Resource or Node. It looks like it does not work pretty well in JavaScript, but I need to have possibility to return custom objects with multiple properties.
The question is: is it even possible to do something similar using Java Use-Api? What do I have to return? I can't return a map, because it won't be possible to access its elements since it's not possible to pass a parameter to Java Use-Api method... Any suggestion?
It is possible to return maps using the java-use api see an example below:
Method in the Java class
//Return a map
public Map<String, String> getTestMap() {
//TODO some coding
Map<String,String> testMap = new HasMap<String,String>();
testMap.put("IDA", "test value");
testMap.put("IDB", "test value 2");
return testMap;
}
HTML code to access each element of the map:
<div data-sly-use.param="JavaClass">
<div data-sly-test.map="${param.testMap}">
<div class="pos">
<span class="classA">${map['IDA']}</span><br>
<span class="classB">${map['IDB']}</span>
</div>
</div>
</div>