Angular Reactive Form Array with freeform object - javascript

I've been working with Angular for a bit and I know my way decently around Reactive forms and template-driven forms but I am having trouble with this problem, especially Angular's Reactive Form Array.
API response that I wish to POST/PUT to is formed like this, where the user is able to add a row and a dropdown will let them select the property, in this case, it's "p", "h1", "h2" and so on and the value they wished to have typed
"description": {
"body": [
{
"p": "test paragraph"
},
{
"h1": "test header"
}
],
I am in the process of converting our old JQuery and js nonsense to a Framework and it has been hard for me to wrap my mind on how to convert this process.
The user will click on Add item:
Then a row will be created via formArray I am assuming since I have tried this beforehand and it has worked but the dropdown to edit the property is giving me an issue.
And this is how it would look like when a few additional rows are created
I understand how formArrays work via this example:
Dynamically Add Rows Based on Condition Using Reactive Forms in Angular
so I will need a getter, as well as the function that creates the dynamic form and HTML but I am currently stuck at the moment.

I have eventually figured out most of the problem,
we will be using Angular's FormBuilder, FormArrays, and template interpolation.
Start by defining the form with form builder:
text-form.ts
this.textForm = this.fb.group({
lang: null,
description: this.fb.array([]),
});
You will be using Angular's getter in this case you want to grab description from the defined above form builder
get descriptionItems() {
return this.artifactTextForm.get('description') as FormArray;
}
Then you will create two functions one is used in a button to add rows and the other to delete the current row at the index in the Array
This will be a bit different from other examples as we will be using some ES6 tricks
addDescriptionItem() {
this.descriptionItems.push(this.fb.group({[this.selectedHeading]: ''}));
}
I defined this.selectedHeading as:
at the top of my component class
selectedHeading = 'h1';
Now create the button that will be created in the forloop on the template for each row
// deleteDescriptionItem removes the item at the index given in the formArray
deleteDescriptionItem(index: number) {
this.descriptionItems.removeAt(index);
}
Now comes one of the most important parts in the template to make sure everything looks nice. I am using Angular Material Design but it should work and perform the same natively.
<form [formGroup]="artifactTextForm">
<mat-dialog-content>
<mat-form-field appearance="fill">
<mat-label>Select Header Type</mat-label>
<mat-select [(value)]="selectedHeading">
<mat-option value="h1">Header 1</mat-option>
<mat-option value="h2">Header 2</mat-option>
<mat-option value="h3">Header 3</mat-option>
<mat-option value="h4">Header 4</mat-option>
<mat-option value="p">Paragraph</mat-option>
</mat-select>
</mat-form-field>
<!-- Description form array -->
<div formArrayName="description">
<div *ngFor="let item of descriptionItems.controls; let i = index" [formGroupName]="i">
<input
[formControlName]="this.selectedHeading"
placeholder="Enter Description Data"
[maxLength]="formMaxLength">
<button
mat-raised-button
color="primary"
type="button"
(click)="deleteDescriptionItem(i)">Delete Item</button>
</div>
<button mat-raised-button color="primary" type="button" (click)="addDescriptionItem()">Add Description</button>
</div>
</mat-dialog-content>
<mat-dialog-actions>
<button mat-raised-button type="submit" (click)="onSubmit()">Save</button>
<button mat-raised-button type="reset" (click)="dialogRef.close()">Cancel</button>
</mat-dialog-actions>
</form>
Make sure the add button is outside of the div with the forloop and it should something like so.
side note:
Flame shot on ubuntu is amazing.

Related

How can I make my custom review stars display required in angular form

I have a form for adding reviews, and added a custom star rating component. The 'rating' form value is required, so it won't let you submit without selecting a rating, but there is no feedback. So I would like it to highlight the review star section if it has no value on submit as if it were an angular form field.
<div class="stars-wrapper" title="Rating: {{ form.get('rating').value }} out of 5 stars">
<div class="star-button-wrapper" *ngFor="let i of [1,2,3,4,5]">
<button class="star-button" type="button" (click)="setRating(i)" attr.aria-label="Set Rating to {{i}} stars" >
<mat-icon class="full-star" *ngIf="form.get('rating').value >= i">star</mat-icon>
<mat-icon class="empty-star" *ngIf="form.get('rating').value < i">star_border</mat-icon>
</button>
</div>
</div>```
However you want to to "highlight" the rating if not chosen you can do it by checking the formcontrol validity. setRating function by my assumption sets the value for the formcontrol. So do you want to display a message if not chosen, well then just check that if
form.get('rating').value > 0
...and any other checks you might want to do, like has the form been submitted?
Want to attach a class to the field if it is not valid, well, then you could use something like:
[ngClass]="form.get('rating').value > 0 ? 'valid': 'not-valid'"
I see you are using angular material so you might want to tinker with the conditions if you are using the default angular material default settings for when the error messages are shown. But with checking the validity of the formfield you can apply styles etc to mimic "normal" material behavior.

How to duplicate div in angular

I am using innerHtml to show html content from variable in angular but it is not supporting external style and ngFor also.in the following screen whenever user click on the add service button then one row should increment.
this is the data having in service list with.
<div class="col-sm-6 col-md-4">
<label class="mobileNumberLabel " for="mobilrNumber">Select Service</label>
<div class="nice-wrap">
<select class="js-example-basic-single nice-textbox" style="width: 105%">
<optgroup *ngFor="let item of serviceObject" label='{{item.categoryName}}'>
<option *ngFor="let service of item.services">
{{service.serviceName}} - <span >{{item.categoryName}}</span>
</option>
</optgroup>
</select></div>
</div>
I tried with innerHtml
<div [innerHTML]="divItem"></div>
but it is showing like this
Angular doesn't work the way you envision it to work, based on your question. When you write a template in Angular, it's actually not HTML -- it's a custom Angular syntax known to the Angular compiler (ngc). Your templates are transformed to TypeScript code which manipulates the DOM based on the model (which is then transformed to JavaScript by tsc so it can be executed by the browser).
This transformation is happening while you're compiling your code. When it reaches user's browsers, there's nothing of Angular templates left in it: no *ngFor, no {{ interpolation.
Therefore, setting innerHTML to Angular's syntax (such as *ngFor or {{ }}) won't work, because you're trying to do in run-time of the application. You'll have to tweak your logic and the way you're building templates to a more structural way.
Instead of setting a string with {{ a }} which you [innerHTML] into an element, switch your logic so that you have an object, for example, { a: 'value' }, and pre-define your template such as {{ obj.a }} in that element.
I think you want to add a reactive form dynamically into the HTML.
I came across this situation some time back as I had to add new paragraphs dynamically.
In this code, I have a text area as input for the paragraph. I can add a paragraph by clicking on the Add New Paragraph button and delete a paragraph by clicking on the Remove button.
.html file should have -
<div formArrayName="contentDescriptions">
<div *ngFor="let contentItem of contentDescriptions.controls; let i=index">
<textarea [formControlName]="i" name="" id="" cols="30" rows="6" placeholder="Enter Description"></textarea>
<button (click)="deleteContentDescription(i)"> Remove</button>
</div>
<div>
<button (click)="addNewParagraph()">
<span class="icon icon-plus"></span> Add New Paragraph
</button>
</div>
</div>
.ts file should have -
form = this.fb.group({
contentDescriptions: this.fb.array([
this.fb.control('')
])
});
get contentDescriptions() {
return this.form.get('contentDescriptions') as FormArray;
};
constructor(private fb: FormBuilder) { }
ngOnInit(){
this.form.patchValue({
contentDescriptions: this.contentSelected.descriptionArray
});
}
addNewParagraph(){
this.contentDescriptions.push(this.fb.control(''));
}
deleteContentDescription(i){
this.contentDescriptions.removeAt(i);
}

nickperkinslondon angular treeview expand_all() problems

I'm using this angular treeview project:
https://github.com/nickperkinslondon/angular-bootstrap-nav-tree
I think that this treeview haven't got functions to do searches over treeview, so I implemented mine using a form to write the label to find.
<form name="searchForm" novalidate style="margin-bottom: 50px">
<div> <input ng-model="search" type="text" class="form-control" placeholder="Buscar..." name="searchTerm" required />
</div>
</form>
This form has a .watch to detect when the user writes some text:
$scope.$watch('search', function(newTerm, oldTerm) {
filteredResponse = !newTerm ? response : updateFilteredResponse(normalize(newTerm));
updateTree();
}, true);
The function 'updateFilteredResponse' filter the nodes with label containing newTerm over original data set (read from json) and returns an array with the items to show in treeview.
The 'updateTree' function use this array and transform my custom items in the array to treeview items to add to the treeview. This items are added to
$scope.tree_data = [];
And this array is the one that uses abn-tree directive:
<abn-tree tree-data="tree_data" tree-control="my_tree" ng-if="loaded" expand-level = "2"></abn-tree>
This part is working fine. My problem comes when the result treeview is shown at screen, the treeview always appears completely collapsed.
If I put a button similar to the library example code like this:
<div style="vertical-align:top">
<button ng-click="my_tree.expand_all()" class="btn btn-default btn-sm">Expand All</button>
</div>
And declaring this in the controller as the example:
var tree;
$scope.my_tree = tree = {};
When the users click the button to expand all over the search results, it works fine. By I need to auto-expand the treeview after a search, and remove the expand-all-button.
For that, I'm trying to call my_tree.expand_all() in my controller. I tried different calls:
$scope.my_tree.expand_all();
tree.expand_all();
In different parts of my controller and my html (using ngIf and onload directives). Even I tried to do a 'watch' over $scope.my_tree for try to use the expand_all() function when var is prepared but I always have the same error:
$scope.my_tree.expand_all is not a function
Can anyone help me with that please?
You can put expand_all() function into setTimeout function as below.
setTimeout(function() {
$scope.my_tree.expand_all();
$scope.$digest();
}, 0);
It have to do because it take some delay time while binding data to treeview.

Submit a form made of multiple components

I've started Angular2 lately and I am facing a problem. I want to create a form to generate some multiple choices questions and this is my problem :
I have a FormComponent which display the layout of my form.
The AnswerComponent can be added to the FormComponent to provide multiple choices about the question.
I have been using the DynamicComponentLoader to programatically add thoses AnswerComponent inside of my FormComponent.
The thing is the submit button must belong to the FormComponent and I do not know how to alert my AnswerComponent to send their data to the FormComponent so it cans gather all the data and create my question.
If anyone has an idea that would be great !
Thank you !
Let's take a sample. I have a form that manages company details:
<form [ngFormModel]="companyForm">
<field label="Name" [state]="companyForm.controls.name">
<input [ngFormControl]="companyForm.controls.name" [(ngModel)]="company.name"/> {{name.valid}}
</field>
<field label="Tags">
<tags [(labels)]="company.labels"></tags>
</field>
<button type="submit" [disabled]="!companyForm.valid">Submit</button>
</form>
As you can see, I use two sub components:
The field one that aims to build the layout for a field block using Bootstrap3. It accepts a variable area to provide the form element (input, select, textarea). This component also leverages the associated control to display validation errors if any.
The tags one that manages the tags attribute that is a list of string. It allows to display, add and remove tags.
You can see that every form element leverages two way binding. This means that each form element is associated to a property of an object of the component. Here it's the company one that is a property of the component.
This means that when you want to submit the form, you can use this company object to build the payload of the corresponding HTTP request for example.
Let's deal a bit more with the associated with the company object. For inputs, it's obvious using the ngModel directive with this syntax: [(ngModel)]. With the tags sub component, it's perhaps not so obvious.
In fact you need define inputs and outputs to manage the labels with two ways bindings:
#Input labels:string[]
#Output labelsChanged: EventEmitter
When labels are updated, you need to call the emit method of labelsChanged.
Here is the complete code for the TagsComponent component:
#Component({
selector: 'tags',
template: `
<div *ngIf="labels">
<span *ngFor="#label of labels" style="font-size:14px"
class="label label-default" (click)="removeLabel(label)">
{{label}} <span class="glyphicon glyphicon-remove" aria-hidden="true"></span>
</span>
<span> | </span>
<span style="display:inline-block;">
<input [(ngModel)]="labelToAdd" style="width: 50px; font-size: 14px;" class="custom"/>
<em class="glyphicon glyphicon-ok" aria-hidden="true" (click)="addLabel(labelToAdd)"></em>
</span>
</div>
`
})
export class TagsComponent implements OnInit {
#Input()
labels:string[];
#Output()
labelsChange: EventEmitter;
constructor(private elementRef:ElementRef) {
this.labelsChange = new EventEmitter();
}
removeLabel(label:string) {
var index = this.labels.indexOf(label, 0);
if (index != undefined) {
this.labels.splice(index, 1);
this.labelsChange.emit(this.labels);
}
}
addLabel(label:string) {
this.labels.push(this.labelToAdd);
this.labelsChange.emit(this.labels);
this.labelToAdd = '';
}
}
Hope it helps you,
Thierry

Aurelia Custom Elements data binding

I am using a custom-element with my Aurelia app. When I am binding data from my view-model with the custom-element, it is working fine. Even if I make some changes in the data in the custom-element control, the change is also reflected to the data in my view model, thanks to the two-way data binding.
However, if I make some changes in the data from the view model (using javascript), the data is not updated in the custom-element. I have replicated this problem for a simpler setting.
Simple View Model
export class Playground {
public testObj: any;
counter = 0;
constructor() {
this.testObj = {
prop1: "This is prop1 value"
, collection2: [{ id: "item21" }]
}
}
updateProp1() {
alert("before update: "+this.testObj.prop1);
this.testObj.prop1 = "Sayan " + this.counter++;
alert(this.testObj.prop1);
}
verifyChange() {
alert(this.testObj.prop1);
}
}
Simple View
<template>
<h1>
Playground
</h1>
<div >
<div repeat.for="item of testObj.collection2">
<div class="row">
<div class="col-sm-4">
<input type="text" class="form-control" placeholder="prop1"
value.bind="$parent.testObj['prop1']">
</div>
</div>
</div>
<button click.delegate="updateProp1()" class="btn btn-primary"> Update Prop1 </button>
<button click.delegate="verifyChange()" class="btn btn-primary"> Verify Change </button>
</div>
</template>
Now whenever I click Verify Change after changing the value in textbox, the changed value comes in the alert. But, if I click the Update Prop1 button, the prop1 value gets updated, but the change doesn't reflect in the view.
I am not sure exactly what I am missing.
Note: Instead of using $parent.testObj['prop1'], if $parent.testObj.prop1 is used, the databinding works as it should. However, my actual custom-element is of generic kind and the property name is not known before hand, hence it seems that I need to keep using $parent.testObj['prop1'] notation instead of dot notation: $parent.testObj.prop1.
At pointer/suggestion/feedback is appreciated.
Update: If anyone can point out the the difference between the dot notation and indexer notation w.r.t. aurelia data-binding (I have checked this already), that will be of great help.
This was an issue in earlier builds of the aurelia/binding module. Here's the related issue(s):
https://github.com/aurelia/binding/issues/75
https://github.com/aurelia/binding/pull/76
I tried your view/view-model in the latest version of aurelia and everything worked. Here's a screencast of what I saw: http://screencast.com/t/KqcuqXWjkU2
Make sure you have the latest version of the aurelia components installed- update steps here: http://blog.durandal.io/2015/05/01/aurelia-may-status-and-releases/

Categories

Resources