I have a simple form with its input field associated to a directive:
<form id="scrollable-dropdown-menu">
<input class="input-field" name="value" [someDirective] type="text" [(ngModel)]="value" #query="ngModel" />
<button (click)="onPost(query.value)" type="submit">Search</button>
</form>
The directive changes the input field through the use of a third party library. In my particular case, this is an autocomplete/typeahead Jquery plugin. This plugin provides options to the user and after selecting an option, it changes the value of the input field.
However, Angular doesn't update its property query.value and therefore, passes the old value to the onPost method.
The directive looks something like this:
#Directive({
selector: '[someDirective]',
})
export class MyDirective {
constructor(private elRef: ElementRef, private renderer: Renderer) {
// this changes the value of the field if the user selects an option
$('.input-field').plugin();
}
}
I was suggested to use UpdateValue, but I can't see how to use it inside a directive. That led me to look at #ViewChild, but it doesn't seem to work in directives, although I could be mistaken.
I also tried to force an update by injecting ChangeDetectorRef, but I didn't see any difference. This is what I did:
my.directive.ts
import {ChangeDetectorRef} from '#angular/core';
#Directive({
selector: '[someDirective]',
})
export class MyDirective {
constructor(private elRef: ElementRef, private renderer: Renderer, private detectorRef: ChangeDetectorRef) {
$('.input-field').plugin();
$('.input-field').my-plugin(':selected', ()=>{
// do something...
this.detectorRef.detectChanges();
})
}
}
AfterSelection is triggered when the user selects an option from the autocomplete plugin. For the plugin, it looks a bit different because it binds to some event, but I think this illustrates the idea.
I would appreciate any suggestions.
UPDATE:
This is a plnkr to show the main issue using the typeahead.js library. If you write a letter in the input field, it will show you some options. Select one of them and the value of the input field will change accordingly. However, when you submit the form (clicking on the input field or pressing enter), an alert will show that the value that was passed to the onPost function was the old value of the field and not the autocompleted value you selected.
Provisional solution: Demo. Passing the form to the custom directive and using updateValue to manually change the value:
my.component.ts
<form *ngIf="active" #frm="ngForm">
<input name="value" [myDirective]="frm" type="text" [(ngModel)]="value" #query="ngModel" />
<button (click)="onPost(query.value)" type="submit">Search</button>
</form>
my.directive.ts
#Directive({
selector: '[myDirective]',
})
export class MyDirective {
#Input('myDirectivev') query;
...
$(this.elRef.nativeElement).my-plugin(':selected', (ev, suggestion) => {
this.query.controls['value'].updateValue(suggestion);
});
This works, but let me know if there is a standard approach to solve this kind of issue.
I was told about another solution on Gitter. Unfortunately, I can't remember who told me about it and Gitter's search functionality doesn't help me. Since this person hasn't posted his solution, I will share it for the benefit of people who might be interested.
The idea is basically to pass the whole field as an argument to the onPost(input) method and access the value property of the input field (input.value).
#Component({
selector: 'my-app',
template: `
<form id="scrollable-dropdown-menu" class="search-form">
<input name="value" class="form-control typeahead" typeahead type="text" data-provide="typeahead" [(ngModel)]="thing" #query />
<button (click)="onPost(query)" class="btn btn-default search-button" type="submit">Search</button>
</form>
`,
directives: [TypeaheadDirective],
providers: []
})
export class AppComponent {
onPost(input) {
alert(`Your current value is ${input.value}`);
}
}
Notice the [(ngModel)]="thing" and #query in the input field. I'm not entirely sure why this updates its value correctly and the native attempt doesn't, but this is far simpler than other alternatives. Here you can find a demo.
Instead of
this.detectorRef.detectChanges();
use
$('.input-field').trigger('input');
ControlValueAccessor used by ngModel listens to input events for normal input fields.
Günter Zöchbauer is right. You should dispatch 'input' event to update angular's model. But JQuery trigger didn't help. Only native event fixed this
#Directive({
selector: '[myDirective]',
})
export class MyDirective {
...
$(this.elRef.nativeElement).my-plugin(':selected', (ev, suggestion) => {
const inputEvent: any = document.createEvent('CustomEvent');
inputEvent.initEvent('input', true, true);
el.dispatchEvent(inputEvent);
});
}
I think Zone is not aware of third-party variable changes, hence third-party variable changes are not reflected in Angular
You could try to place your jQuery code inside an angular zone, by implementing something like this:
import Zone: import {NgZone} from 'angular2/core' or import {NgZone} from '#angular/core' , depending on the Angular 2 version you are using;
add zone: NgZone to the constructor arguments of MyDirective;
replace $('.input-field').plugin(); by
zone.run( () => {$('.input-field').plugin(); });
Here is a post about Change detection and Zone
Hope this will help you...
Related
My html file has the following in it:
<form (submit)="register()">
<div class="form-group">
<label for="username">Username</label>
<input type="text" name='username' placeholder="Enter your username" [(ngModel)]="userData.username">
In my ts file for the same component I have:
userData: any = {};
Finally, in my app.module.ts I have:
import { NgModule } from '#angular/core';
#NgModule({
...,
imports: [ ... FormsModule, ...],
...})
According to everything I've read, that is sufficient to have the data in my .ts file update, but it is not currently. I've tried manually updating ngModelChange seperately than the model (and updating the [] and () accordingly), changing names across the html file, removing elements from html and adding them back to find if something isn't working, and several other desperation tricks, but can't seem to figure out why this isn't properly binding.
Form control and ngModel shouldn't be implemented at the same time. If you're going to implement a form group, then use a form control. Otherwise, use ngModel.
I am developing a simple bookmarks chrome extension and using Angular as a framework, probably not the best choice for such tasks, but I wanted to try it out.
Following this article here it all seems logical. However, I started running into very strange problems.
When I click on the bookmark Icon, the bookmark view inputs are not populated with values from chrome API (seems that Angular doesn't fire up change detection or something like this, I'm new to front-end and Angular in general). In order for the view to change I need to click on one of the inputs then click on another input after that the view changes.
Another observation is when I use <select> input when I select an option the input doesn't change I need to click on one of the text inputs in order to see the change. I believe the inputs must be filled up as the code chrome.tabs.query is executed in the component's constructor. I suspect that this code runs outside of Angular hence angular don't fire change detection so I tried ChangeDetectorRef.detectChanges() and it worked for updating inputs when I click on bookmark icon but this didn't solve the issue <select> so the problem might be even deeper.
Here is the components code:
import {Component, OnInit} from '#angular/core';
#Component({
selector: 'app-add-bookmark',
templateUrl: './add-bookmark.component.html',
styleUrls: ['./add-bookmark.component.css']
})
export class AddBookmarkComponent implements OnInit {
url: string = "";
directory: string = "";
description: string = "";
tags: string[] = [];
image: string;
recentFolders = [];
folderId: string = "2";
constructor() {
chrome.tabs.query({active: true, currentWindow: true}, tabs => {
let activeTab = tabs[0];
this.url = activeTab.url;
this.description = activeTab.title;
this.image = activeTab.favIconUrl;
// ChangeDetectorRef.detectChanges(); //this worked and inputs are updated but It didn't solve the issue with Select.
});
}
}
HTML:
<div class="container align-items-center p-2">
<div class="row align-items-center">
<div class="col-sm-1">
<div class="mb-1">
<img *ngIf="image" [src]="image"/>
</div>
<div class="mb-1">
<label for="url" class="form-label">URL</label>
<input type="text"
class="form-control form-control-sm" id="url"
aria-describedby="url" [(ngModel)]="url" name="url">
</div>
<div class="m-1">
<label for="description" class="form-label">Description</label>
<textarea type="text" class="form-control form-control-sm" id="description" aria-describedby="desc"
[(ngModel)]="description" name="description"></textarea>
</div>
<div class="m-1">
<label for="description" class="form-label">Select Folder:</label>
<select id="selected_dirs" class="form-select form-control form-control-sm"
aria-label="Default select example"
name="directory">
<option *ngFor="let f1 of recentFolders" [ngValue]="f1.id">{{f1.name}}</option>
</select>
</div>
<div class="mt-4 mb-2">
<button class="btn btn-primary btn-sm" (click)="openManager()">Open Manager</button>
<button class="btn btn-dark btn-sm m-2" (click)="addBookmark()">Add bookmark</button>
</div>
</div>
</div>
</div>
Here is a visual demo
It is supposed to fill URL and Description
Click on an input field, nothing happens
Click on another input field and it works
Here is the "inspect" window from the bookmark.
Thanks for taking some time to read this long question.
What I believe is, the issue is the angular detection. It works like that whenever there is any change in the data in the component angular runs change detection to render this change on the view. There can be two strategy OnPush or default. You can enable it like this:
#Component({
selector: 'app-add-bookmark',
templateUrl: './add-bookmark.component.html',
styleUrls: ['./add-bookmark.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush. //you can change it to default ChangeDetectionStrategy.Default
})
In this case(default) whenever your data changes in the component, angular runs this by itself. But it makes the things little laggy, you check it if that works for you.
So, My suggestion is whenever you make any change in the view, you need to run the change detection to update the view. To do that, add a Hostlistener and whenever user clicks on this component you can fire change detection.
import { HostListener } from '#angular/core'; // add in component file
#HostListener('click', ['$event'])
onMouseEnter(event: any) {
//Run change detection.
this.changeDetection();
}
public changeDetection() {
this.ChangeDetectorRef.detectChanges();
}
This listener will be restricted to this component only. If you want to get more details about change detection: https://blog.angular-university.io/how-does-angular-2-change-detection-really-work/
Also, If there is any context issue that you face while debug you can use Ngzone: https://angular.io/guide/zone
I am attached the custom select component from visual design. I need to reproduce it. up to my level I am not able to customize the select component to mach this mock.
But as a option, I can able to create a drop down using ul to match the same. But In my angular app, I am using Reactiveform, so, where I could not able to add the validation errors.
so what is the correct way to handle this kind of scenario?
Mytry
here is my mock up:
Using ReactiveForms you can set Validators to any of your Form Controls
public readonly form: FormGroup = this.fb.group({
SelectedValue: new FormControl("",Validators.required)
});
Then in your template you can use any options you want defined in your component or otherwise, and set some flags and helpful messages when the form control is not valid.
Validators.required will make sure that the field is required for the form to be valid
<div [formGroup]="form">
<select formControlName="SelectedValue">
<option *ngFor="let opt of options" [value]="opt.key">{{opt.value}}</option>
</select>
<h3 *ngIf="!form.controls.SelectedValue.valid">Selected Value field is required</h3>
<button disabled="!form.controls.SelectedValue.valid" (click)="doSomething()"> Click me</button>
</div>
Your component (Do not forget the imports!):
****
import { FormBuilder, FormGroup, FormControl, Validators } from '#angular/forms';
****
constructor(private fb: FormBuilder){}
options = [{key:1,value:"A"},
{key:2,value:"B"},
{key:3,value:"C"}];
public readonly form: FormGroup = this.fb.group({
SelectedValue: new FormControl("",Validators.required)
});
doSomething(){
//ur logic goes here
alert(this.form.value.SelectedValue);
}
Another option is to use ng-select if you are using angular bootstrap in your project which has some extra functionality like clear and styling.
Im currently written a directive to scrollIntoView for all the textboxes in the application. Adding the directive to thousands of textboxes in the application is painful.
Is there any interceptor kind of thing where i can intercept all focus events of the textbox.
I tried something like this and it works.
import { Directive, OnInit, HostListener } from '#angular/core';
#Directive({
selector: "[input[type='text']]"
})
export class InputTypeDirective implements OnInit{
ngOnInit(){
console.log('loading directive');
}
#HostListener('focus', ['$event'])
onFocus(e){
console.log(e);
}
}
Template:
<input type="text" [(ngModel)]="username" value="">
Also include the directive as declaration in module.
I want to create the following component in ember (templates / components / echo-hlabel-tf.hbs)
<div id="echo-hlabel-tf">
<span id="tf-label">{{label}}</span>
<span id="tf-value">
<input type="text" value={{textValue}}
{{action "validateRegExp" textValue regExp post on="focusOut"}}
{{action "showText" textValue post on="mouseUp"}}
/>
</span>
</div>
Just a textfield and a label placed in the same row . I have created the functions in the controller of the component (components / echo-hlabel-tf.js):
import Ember from 'ember';
export default Ember.Component.extend({
actions: {
validateRegExp(text,regExpStr){
let regExpObj = new RegExp(regExpStr)
if(regExpObj.test(text)){
console.log('entry matches')
}else{
console.log('entry is wrong');
}
},
showText(text){
console.log(text);
}
}
});
I want to use this component from another template (template / programmers.hbs) :
<ul>
{{people-list title="List of programmers" people=model}}
</ul>
<div>
{{echo-hlabel-tf
label="Textfield horizontal"
textValue="..."
regExp="^([a-z0-9]{5})$"
}}
</div>
The thing is that , even the actions are fired as the events trigger, the variable that represents the value of the input (textValue) always stands the same ("...") . seems that the binding between the outer template, and the components is created at the beginning, but if I put more letters in the textfield , the value of textValue remains the same and doesn't change . I would like to, as I'm adding letters to the textBox, print them with the console with the method showText, but I always print "..."
You are using pure html5 input that does not do two way binding by itself. That is even if you type something, it is not reflected to textValue attribute. You can use ember's builtin input component as in this twiddle. I believe this will solve your problem.