Force update of a property binding in angular 2 - javascript

Consider the following component
import {Component} from 'angular2/core'
#Component({
selector: 'my-app',
providers: [],
template: `
<div>
<h3>Input with two decimals</h3>
<input type="text"
[value]="formattedN"
(change)="nChanged($event.target.value)"/>
</div>
`,
directives: []
})
export class App {
private n: number = 0;
private formattedN: string = "0.00"
public nChanged(value) {
this.n = parseFloat(value);
this.formattedN = this.n.toFixed(2);
}
}
The input should always be a number with two decimals. That is however not always the case. Try removing the last zero, the field is not changed to 0.00 which is what I want. I understand it doesn't work because the formattedN value is not updated, which means that the property binding is not updated, hence the value of the input is not updated.
Run the example in plunker: http://plnkr.co/edit/Vyi4RKladslrdZslZQhm
Does anyone have a nice solution for this problem?

Is this Answer what you're trying to accomplish????
//our root app component
import {Component} from 'angular2/core'
#Component({
selector: 'my-app',
providers: [],
template: `
<div>
<input type="text" [(ngModel)]="formattedN" (change)="nChanged($event.target.value)"/>
</div>
`,
directives: []
})
export class App {
private n: number = 0;
private formattedN: string = "0.00"
constructor() {
this.name = 'Angular2'
}
public nChanged(value) {
console.log(value);
this.n = parseFloat(value);
console.log(this.n);
this.formattedN = this.n.toFixed(2)
console.log(this.formattedN);
}
}

Related

Angular-How to use spread operator in angular html template

I have a simple angular app with a module and a component. For the sake of simplicity let us assume that the component ts and the template file is like the following snippet
import {
Component,
Input,
OnInit
} from '#angular/core';
import {
ChildComponent
} from './child/child.component';
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
title = 'test-app';
val;
clickEvt() {
alert("clicked")
}
isTruthy() {
if (( < HTMLInputElement > document.getElementById("inp1")).value == "admin" && ( < HTMLInputElement > document.getElementById("inp2")).value == "admin") {
return true;
}
return false;
}
ngOnInit() {
this.val = {};
this.val.id = "id1";
this.val.class = "abc";
}
}
<label>User</label>
<input {{...val}} (input)="isTruthy()" />
<br>
<label>Password</label>
<input id="inp2" (input)="isTruthy()" />
<button (click)="clickEvt()">Login</button>
<div *ngIf="isTruthy(); then truthy else falsey"></div>
<ng-template #truthy>
<h1>Success</h1>
<child-component [value]="isTruthy()"></child-component>
</ng-template>
<ng-template #falsey>
<h1>Failure</h1>
<child-component [value]="isTruthy()"></child-component>
</ng-template>
In the HTML template, you can notice me trying to use {{ ...val }}. This is my attempt to use spread operator in the template but unfortunately I get the exception
ERROR DOMException: Failed to execute 'setAttribute' on 'Element': '{{...val.id}}' is not a valid attribute name.
I just want to know, is there a way to use spread operator or is there an equivalent way in angular to give multiple attributes in one go which is obtained from a variable?
No, you cannot use the spread in a template (even less like you're trying).
To achieve that, you will need a reference to your template.
<input #myInput (input)="isTruthy()" />
#ViewChild('myInput', { static: true }) myInput: ElementRef<HTMLInputElement>;
ngOnInit() {
Object.assign(this.myInput.nativeElement, this.val);
}
One way to do this could be create a directive like
import { Directive, ElementRef, HostListener, Input } from '#angular/core';
#Directive({
selector: '[setAttr]'
})
export class AttrDirective {
constructor(private el: ElementRef) { }
#Input() attr: any;
ngOnInit(){
if(this.attr){
console.log(this.el.nativeElement)
Object.keys(this.attr).forEach(k=>{
this.el.nativeElement.setAttribute(k,this.attr[k])
console.log(this.el.nativeElement)
})
}
}
}
then apply on input like
<input type="text" setAttr [attr]="val">
demo

Scope class name and script per angular component

I have a component which is repeated various times on a page. I am implementing the AutoNumeric library and need to pass a selector to the function. However, the selector needs to be unique per instance of the component, so the first instance would be cssClass-1 and the second instance cssClass-2, and the js inside the component would know which selector to look for.
import { Component, ViewChild, OnInit } from '#angular/core';
import { FieldType } from '#ngx-formly/material';
import AutoNumeric from 'AutoNumeric';
#Component({
selector: 'app-form-currency-type',
template: `
<div class="cssClass-1">
<input
matInput
[formControl]="formControl"
[formlyAttributes]="field"
autocomplete="false"
type="text"
/>
</div>
`,
})
export class CurrencyTypeComponent extends FieldType implements OnInit {
ngOnInit() {
const anElement = new AutoNumeric('.cssClass-1 > input', {
allowDecimalPadding: false,
caretPositionOnFocus: "start",
currencySymbol: "£",
decimalPlaces: 0
});
}
}
I can't find a way to scope the script to just this instance of the component, is there a way to do this?
You need to make the class name a JS variable so it can be changed for each instance.
If you are happy with random strings maybe use a uuid library.
import { Component, ViewChild, OnInit } from '#angular/core';
import { FieldType } from '#ngx-formly/material';
import AutoNumeric from 'AutoNumeric';
const uuidv1 = require('uuid/v1');
#Component({
selector: 'app-form-currency-type',
template: `
<div [ngClass]="cssClass">
<input
matInput
[formControl]="formControl"
[formlyAttributes]="field"
autocomplete="false"
type="text"
/>
</div>
`,
})
export class CurrencyTypeComponent extends FieldType implements OnInit {
// initialize empty by default
public cssClass = '';
ngOnInit() {
// generate new random string
const cssID = uuidv1();
// prefix required as class names can't start with numbers
const cssPrefix = "cur--";
this.cssClass = cssPrefix + cssID.toString();
// use string interpolation
const anElement = new AutoNumeric(`${this.cssClass} > input`, {
allowDecimalPadding: false,
caretPositionOnFocus: "start",
currencySymbol: "£",
decimalPlaces: 0
});
}
}

Two way binding on Array elements using <form>

I have an array of objects in a component. which I will iterate in the template.
app.component.ts
import {Component, OnInit} from '#angular/core';
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
title = 'sample-app';
classesData = [];
constructor() {
}
ngOnInit() {
this.classesData = [
{title: 'Hello0'}, {title: 'Hello1'}, {title: 'Hello2'}
];
}
duplicate() {
const newData = JSON.parse(JSON.stringify(this.classesData[1]));
newData.title += 'Copy';
this.classesData.splice(1, 0, newData);
}
}
app.template.html
<form #testingFrom="ngForm">
<p>{{classesData | json}}</p>
<div *ngFor="let classData of classesData; let i=index">
<input [(ngModel)]="classData.title" name="{{'title-' + i}}" type="text">
</div>
<button (click)="duplicate()">Duplicate</button>
</form>
My aim is when a user clicks on the duplicate button I simply add a new element at index 1 in an array. My initial state looks like (before user clicks)
And my state after a user clicks duplicate button
In the image above at 3rd input field, we are getting Hello1Copy instead of Hello1.
I completely suspect that this behavior is happening because of conflict in the name attribute value. For this case only, if you splice the newItem at first location, it only adds that's variable and other DOM's doesn't re-render. For cross verification you can try replacing input element with simple binding like {{classData.title}} and everything works fine.
This behavior can easily be solved by not conflicting name attribute value for all time. What that means is to assign a unique id variable with each collection item and use it.
this.classesData = [
{ id: 1, title: 'Hello0' },
{ id: 2, title: 'Hello1' },
{ id: 3, title: 'Hello2' }
];
duplicate() {
const newData = JSON.parse(JSON.stringify(this.classesData[1]));
newData.title += 'Copy';
newData.id = Date.now()
this.classesData.splice(1, 0, newData);
}
Template
<div *ngFor="let classData of classesData;let i=index">
<input [(ngModel)]="classData.title" [name]="'title_'+classData.id" type="text">
</div>
Stackblitz
You can also verify the same by removing name attribute from each input field. But that would not suffice, it would throw
ERROR Error: If ngModel is used within a form tag, either the name
attribute must be set or the form control must be defined as
'standalone' in ngModelOptions.
So add [ngModelOptions]="{standalone: true}" on each input field to make input working without name attribute. As suggested in another answer by #briosheje, you can also re-enforce rendering using trackBy.
PS: I'm investigating why this works differently when there is a combination of name and input, I suspect about form API wiring with input element. I'll update the answer as soon as I get something.
The problem is that you are using a form. Because you're using a form, you need to specify how angular should track the changes for your form items, if you're planning to alter the existing source. You can do such using the trackBy pipe:
<form #testingFrom="ngForm">
<p>{{classesData | json}}</p>
<div *ngFor="let classData of classesData; let i=index; trackBy: trackByFn">
<input [(ngModel)]="classData.title" [name]="'title-' + i" type="text">
</div>
<button (click)="duplicate()">Duplicate</button>
</form>
Typescript relevant part:
trackByFn(index: any) {
return index;
}
Please note that adding elements to the collection will work in your original example.
Working stackblitz: https://stackblitz.com/edit/angular-uabuya
Make another variable and iterate that variable to crate inputs
import { Component,OnInit } from '#angular/core';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent implements OnInit {
title = 'sample-app';
originalData=[];
classesData = [];
constructor() {
}
ngOnInit() {
this.classesData = [
{title: 'Hello0'}, {title: 'Hello1'}, {title: 'Hello2'}
];
this.originalData=[...this.classesData]; // changed here
}
duplicate() {
const newData = JSON.parse(JSON.stringify(this.classesData[1]));
newData.title += 'Copy';
this.classesData.splice(1, 0, newData);
}
}
Working Demo
You can solve your issue using "trackBy" feature. Please see below code sample.
app.component.ts
import { Component, OnInit } from '#angular/core';
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
title = 'sample-app';
classesData = [];
constructor() {}
ngOnInit() {
this.classesData = [
{ title: 'Hello0' },
{ title: 'Hello1' },
{ title: 'Hello2' }
];
}
duplicate() {
const newData = JSON.parse(JSON.stringify(this.classesData[1]));
newData.title += 'Copy';
this.classesData.splice(1, 0, newData);
}
trackByIndex(index: number, obj: any): any {
return index;
}
}
app.component.html
<form>
<p>{{classesData | json}}</p>
<div *ngFor="let classData of classesData; let i=index;trackBy:trackByIndex;">
<input [(ngModel)]="classesData[i].title" name="{{'title-' + i}}" type="text" />
</div>
<button (click)="duplicate()">Duplicate</button>
</form>
Please let me know if this solution works for you!

How to check if two strings are almost equal with Angular

I have a quiz app made with Ionic and Angular 4. User have to submit answer, I check if it's the same as the good answer or not.
I would like to check string correspondence, and handle event according to the correspondence between good answer and user answer.
In Exemple :
If the answer is 'azerty', and he wrote 'mzerty', I would like to allow him to continue.
If user wrote 'qwerty', or something too different, he failes.
A simple demo with Levenstein distance would be like that:
Typescript
import { Component, OnInit } from '#angular/core';
import { FormBuilder, FormGroup } from '#angular/forms';
import { Observable } from 'rxjs/Observable';
import { map } from 'rxjs/operators';
import levenshtein from 'fast-levenshtein';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent implements OnInit {
form: FormGroup;
score$: Observable<number>;
constructor(private fb: FormBuilder) { }
ngOnInit() {
this.initForm();
this.initScore();
}
private initForm() {
this.form = this.fb.group({
str1: '',
str2: '',
});
}
private initScore() {
this.score$ = this.form
.valueChanges
.pipe(
map(({str1, str2}) => levenshtein.get(str1, str2))
);
}
}
HTML
<form [formGroup]="form">
<input type="text" formControlName="str1">
<br>
<br>
<input type="text" formControlName="str2">
</form>
<br>
<div>
Levenshtein score: {{ score$ | async }}
</div>
Stackblitz live demo: https://stackblitz.com/edit/angular-usydyu
You can simply create a method which will return you how many characters are matched. so on basis of matched characters and the length of string you can decide weather its a good answer or not.
function checkEq(str1, str2){
var arr1 = str1.split('');
var arr2 = str2.split('');
var counter = 0;
for(var i=0;i<arr1.length;i++){
if(arr1[i]==arr2[i]){
counter++;
}
}
return counter;
}

How can I pass a variable from #Input to a service in an Angular2 component>

So, What I am trying to do seems like it would be trivial. And it probably is. But I can't figure it out. My question is:How can I pass a variable from #Input to a service in an Angular2 component? (Code has been simplified)
My component is as follows:
import { Component, Input } from '#angular/core';
import { CMSService } from '../cms.service';
#Component({
selector: 'cmstext',
templateUrl: './cmstext.component.html',
styleUrls: ['./cmstext.component.css']
})
export class CMSTextComponent {
constructor(private cms: CMSService) { }
#Input() id : string;
content = this.cms.getContent(this.id); // this.id is NULL so content is NULL
}
And then my service:
import { Injectable } from '#angular/core';
#Injectable()
export class CMSService {
constructor() { }
getContent(textId:string) : string {
this.text = textId; // textId is NULL so this.text returns NULL
return this.text;
}
}
My component template:
<p>id: {{id}}</p>
<p>Content: {{content}}</p>
When <cmstext id="4"></cmstext> is added to another component template the output is:
id: 4
content:
I'm just diving into Angular2 any help or suggestions would be greatly appreciated!
Just make it a setter and put the code there:
#Input()
set id(value : string) {
this.content = this.cms.getContent(value);
}
As pointed out by #Kris Hollenbeck,ngOnInit() was the answer. My final code looked like this. The component now passed the variable to the service.
import { Component, Input, OnInit } from '#angular/core';
import { CMSService } from '../cms.service';
#Component({
selector: 'cmstext',
templateUrl: './cmstext.component.html',
styleUrls: ['./cmstext.component.css']
})
export class CMSTextComponent implements OnInit {
public content : string;
#Input() id : string;
constructor(private cms: CMSService) { }
ngOnInit() {
this.content = this.cms.getContent(this.id);
}
}
This assigned the data from the service to the variable "content" and the id passed from the element attribute to the variable "id". Both variables were then accessible to the template!

Categories

Resources