Angular 2 - How to clear an input with a local variable? - javascript

Following the guide from the Angular2 site I have this html:
<input #something (keyup)="doneTyping($event)">
<button (click)="add(something .value)">Add</button>
with this controller:
#Component({
selector: 'my-app',
appInjector: [SomeService]
})
#View({
templateUrl: 'index-angular',
directives:[NgFor]
})
class MyAppComponent {
name: string;
stuff: Array<string>;
constructor(someService: SomeService) {
this.name = 'Angular2Sample';
this.stuff= someService.getStuff();
}
add(st: string){
this.stuff.push(st);
}
doneTyping($event) {
if($event.which === 13) {
this.stuff.push($event.target.value);
$event.target.value = null;
}
}
}
When the user hits enter in the input, the doneTyping method clears the input with $event.target.value = null;.
However I can't come with a way of doing the same after pushing the button.

You can pass the input as a parameter in the button
<input #something (keyup)="doneTyping($event)">
<!-- Input as paramter -->
<button (click)="add(something)">Add</button>
And later in the add function
add(st: HTMLInputElement){
this.stuff.push(st.value);
st.value = null;
}

Also, you usually want to avoid interacting with DOM as much as possible. I just checked the Todo app example on the angular2 github and they also access the DOM element, but the last real commit is 2 months old.
If you use data binding you can have a cleaner code which would result in something like :
<input [value]="_newStuff" (keyup.enter)="addStuff()">
<button (click)="addStuff()">Add</button>
Then in your class you can just define the member _newStuff : string, that you can implement addStuff as follow :
addStuff() {
this.stuff.push(_newStuff);
this._newstuff = '';
}
In most cases you might want _newStuff to be a model object that works as an interface like this :
class Stuff {
id : any;
property : any;
otherProperty : any;
}
And then your _newStuff would be _newStuff : Stuff; and you could map it like this : <input [value]="_newStuff.property" (keyup.enter)="addStuff()">.
I know your sample code is very simple and you just try to get it to work, but I believe the data binding way is more in the logic of the framework, and the Form API basically gives tools such as Control, ControlGroup and FormBuilder that help you map your model on your view with Validators and such. It would be too cumbersome on something a bit larger to access the DOM everytime you need to change the view. In the end your example is almost raw javascript executed in an Angular2 context.
Coming back to your example, now imagine you have another event that triggers a new stuff to be added, say a double click or so, you'd need to add another method that handles this event, passing it again the HTMLInputElement, and do basically the same thing as you do on the keyup event handler, thus duplicating code again. With data binding your component owns the state of the view and you can therefore have one simple method that won't be affected by what kind of event triggered it. There you can do the test if the model is valid ( even though for this you'd use the Form API then ).
Anyways, I know this has been answered already, but I thought I would just help to improve the solution given my current understanding of it and how it could be applied to real cases.

You can use a one-direction bindings to access the value of the input. This is a very clear architecture; you don't have to pass DOM elements to the controller.
Template:
<!-- controller.value -> input.value binding-->
<input #myinput [value]=myValue>
<button (click)="done(myinput.value)">Add</button>
Controller:
myValue: string; // If you change this then the bound input.value will also change
// be sure to import ngOnInit from #angular/common
ngOnInit() {
this.myValue = "";
}
done(newValue) {
// ... processing newValue
this.myValue = ""; // Clear the input
}

Here's a good way to actually get your input objects to manipulate
Just need to import ViewChild from #angular/core
Template:
<input #something (keyup)="doneTyping($event)">
Class:
#ViewChild("something") something : HTMLInputElement;
doneTyping(e : KeyboardEvent) {
var text = this.something.value;
if (text != "") {
//DO SOME STUFF!!!!
}
}

Since version 2 of Angular is old now, and we developers are in need much more trend solutions and but we may look old topics in order to find solutions here, I felt I should mention an answer from another topic similar to this.
That answer works and solved my problem in Angular 7 with Type Script. Here its link
There are two ways:
1) I it is created with var or let or const, then it cannot be deleted at all.
Example:
var g_a = 1; //create with var, g_a is a variable
delete g_a; //return false
console.log(g_a); //g_a is still 1
2) If it is created without var, then it can be deleted.. as follows:
declaration:
selectedRow: any;
selectRowOfBankForCurrentCustomer(row: any) {
this.selectedRow = row; // 'row' object is assigned.
}
deleting:
resetData(){
delete this.selectedRow; // true
console.log(this.selectedRow) // this is error because the variable deleted, not its content!!
}

A quick way to do it:
<input #something (keyup)="doneTyping($event)">
<button (click)="add(something.value);something.value=''">Add</button>
A more Angular way to do it:
HTML:
<input [(ngModel)]="intermediateValue">
<button (click)="add()">Add</button>
Controller:
intermediateValue: string;
add() {
this.doStuff(this.intermediateValue);
this.intermediateValue= "";
}

Related

Is there a way I can dynamically set the formControlName on an <input> element in angular?

I'm working on writing a component intended to simplify/homogenize the way our forms look and interact. The code looks something like this:
Example Usage
...
<my-form-input labelKey = "email" controlName="emailAddress" [form]="userForm">
<input myInput class="form-control" type="email" formControlName="emailAddress" />
</my-form-input>
...
You can see that "emailAddress" is passed to MyFormInputComponent as the controlName and is passed a second time to the FormControlName directive on the <input> element. I'd like to only pass this once so that my end user doesn't have to do this.
Is there a good way I can go about this, or is this just a constraint I should accept (if yes, an explanation of why this constraint exists would be welcome)? Code is shown below.
I've tried two approaches:
Setting a #HostBinding("attr.formControlName") annotation in the MyInput component. I can manipulate an attribute called formcontrolname on the element this way, but it doesn't trigger the directive that Angular Forms needs to properly register the control with the group.
Ask the user to supply formControlName to the <input> element and read the value off of this for the rest of the component. This might work, but I'd have to access the DOM directly through an ElementRef, which is not recommended. The recommended route for interacting with DOM -- Renderer -- doesn't seem to expose any ability to read attributes either.
my-form-input.component.ts
#Component({
selector: 'my-form-input',
templateUrl: './my-form-input.component.html',
styleUrls: ['./my-form-input.component.scss']
})
export class MyFormInputComponent implements OnInit, AfterContentInit {
#Input()
labelKey: string;
#Input()
controlName: string;
#Input()
form: FormGroup;
#ContentChild(MyInputDirective)
input: MyInputDirective;
ngAfterContentInit(): void {
this.initInput();
}
/**
* Updates the input we project into the template
*/
private initInput() {
this.input.updatePlaceholder(this.labelKey);
// I'd like to somehow edit the FormControlName directive applied to the input here
}
}
my-form-input.component.html
<label>{{ labelKey | translate }}</label>
<ng-content></ng-content>
<my-input-error [control]="form.controls[controlName]" [name]="labelKey | translate" />
my-input.directive.ts
#Directive({
selector: '[myInput]'
})
export class myInputDirective implements OnInit {
private placeholderKey = "";
#HostBinding("placeholder")
private placeholder: string;
updatePlaceholder(placeholderKey: string) {
this.placeholderKey = placeholderKey;
this.placeholder = this.translateService.instant(this.placeholderKey);
}
constructor(private translateService: TranslateService) {
}
}
my-form-error.component.ts
// Not shown since not relevant.
I'm still not sure the exact explanation, but some reasoning alludes to where I might have strayed.
I assumed that my component owned the elements that it was projecting, but I actually think this isn't true. Your opportunity to set attributes/directives is in the template. This means you are better off including any elements that you want to own/control into the template rather then just projecting them.
In this case, that leads you to making separate components for specific form controls (like <input>, <textarea>, etc). This is what I've ended up doing. This is better from a design endpoint anyway -- one component to wrap all possible form-controls was never going to happen. Either project a form control that duplicates some properties, like I do in the question, or create specific components. Or both (just make your most common form controls into components, wrap the one-off problems in your projecting component).
Here are some blogs that helped me find my way:
https://medium.com/#vadimkorr/implementing-nested-custom-controls-in-angular-5-c115c68e6b88
https://blog.angularindepth.com/never-again-be-confused-when-implementing-controlvalueaccessor-in-angular-forms-93b9eee9ee83

Angular 5 + Electron + Generated TOC

I have a Angular Pipe:
import {Pipe, PipeTransform} from '#angular/core';
import { DomSanitizer } from '#angular/platform-browser';
import * as Remarkable from 'remarkable';
import * as toc from 'markdown-toc';
#Pipe({
name: 'MarkdownToc'
})
export class MarkdownTableOfContentsPipe implements PipeTransform {
constructor(private sanitized: DomSanitizer) {}
public transform(markdown: string) {
const toc_opts = {
linkify: function(tok, text, slug, options) {
const regex = /(.+\b)(.*)$/
slug = slug.replace(regex, function(str, g1) { return g1; });
tok.content = `[${text}](#${slug})`;
return tok;
}
}
const toc_md = new Remarkable('commonmark')
.use(toc.plugin(toc_opts))
const md = new Remarkable('commonmark')
md.renderer.rules.link_open = function(tokens, idx, options /* env */) {
var title = tokens[idx].title ? (' title="' + Remarkable.utils.escapeHtml(Remarkable.utils.replaceEntities(tokens[idx].title)) + '"') : '';
var target = options.linkTarget ? (' target="' + options.linkTarget + '"') : '';
return '<a href="/articles' + Remarkable.utils.escapeHtml(tokens[idx].href) + '"' + title + target + '>';
};
const toc_md_text = toc_md.render(markdown);
console.log(md.render(toc_md_text.content));
return this.sanitized.bypassSecurityTrustHtml(md.render(toc_md_text.content));
}
}
It generates a list of links (this a shortened list):
<ul>
<li>Introduction</li>
<li>Downloads</li>
</uL>
However, every link shows up was "file:///" + href which of course won't work. Is there some way to fix the hrefs to get it to work or some other way.
In my controller, I have this function:
private async _show() {
const db = await this.databaseService.get();
const id = this.route.snapshot.paramMap.get('id').split('-').join(' ');
const article$ = db.article
.findOne(toTitleCase(id))
.$;
this.sub = article$.subscribe(async article => {
this.article = article;
const attachment = await this.article.getAttachment('index.md');
this.text = this.markdownPipe.transform(await attachment.getStringData());
this.intermoduleservice.toc = this.markdownTocPipe.transform(await attachment.getStringData());
this.zone.run(() => {});
});
}
The InterModuleService is a global service to push the TOC to my Side Nav menu where the TOC is being located. It seems when I push the TOC html to the Side Nav through this service, there is no rendering updates performed on the HTML. So [routerLink] bindings or Angular specific code never gets updated properly.
So here's what I would do in your case. The href links themselfs are only a workaround because you are probably setting html as a plain string, which angular cannot take hold of and therefore no angular specific code like routing can be injected like that. This is not ideal because it results in a full page reload and another full angular initialization, which ultimately takes a lot of time.
Let's see if we can make it faster by trying to implement native angular routing. Just to be clear, I don't know if this works for Electron. What you can do is refer to the html tag where you're injecting the markdown with #ViewChild(). I'm talking about the last level of html that was created through an angular angular component. Let's say it would be a div that has an innerHTML attribute where you're using your pipe on.
<div [innerHTML]="originalMarkdown | MarkdownToc" #markdownDiv></div>
Now this way the inner HTML is completely inserted as a string without angular knowing what's inside, so no angular stuff works inside of this div. What you can do though is using the reference on that div to walk down the HTML tree of the innerHTML after it was created.
#ViewChild('markdownDiv') markdownDiv: ElementRef;
// this hook makes sure the html is already available
ngAfterViewInit() {
// find a link tag inside your innerHTML
// this can be any selector, like class, id or tag
var link = this.markdownDiv.nativeElement.querySelector("a");
// ... do something with the link
}
You said you have multiple links, maybe you have to use another selector or even give each link an ID in the html creation and refer to that here. As soon as you have a reference to the link you can use it to create an onClick function that can for example use routing. We need something called the Renderer2 here that helps us with it. It is injected as a normal service. Here's the majority of the code you need which can be placed in your component.ts.
constructor(private renderer: Renderer2, private router: Router){}
#ViewChild('markdownDiv') markdownDiv: ElementRef;
// this hook makes sure the html is already available
ngAfterViewInit() {
// find a link tag inside your innerHTML
// this can be any selector, like class, id or tag
var link = this.markdownDiv.nativeElement.querySelector("a");
// ... on click
this.renderer.listen(link, 'click', (event) => {
this.router.navigateByUrl("/articles#introduction");
});
}
Now that's a lot to do but it may solve your problem with a faster implementation than you are trying to do it right now. If you have any questions, feel free to ask.
Edit
I suppose the life cycle hook ngAfterViewInit() described here might just be too early to query your element, because in this stage the DOM has not been updated yet. If I see it correctly, you are making a database call to create your markdown, which takes time. Angular usually just inits the view of your template and does not rely on dynamic DOM manipulation afterwards.
What we have to do is probably hook into a later stage. This is a bit more tricky, but should still be managable. What we're gonna use is ngAfterViewChecked().
ngAfterViewChecked()
Respond after Angular checks the component's views and child views.
Called after the ngAfterViewInit and every subsequent
ngAfterContentChecked().
It's pretty similar to the ngAfterViewInit(), we just have to make sure that we don't create the click listener multiple times on the same <a> tag, because the function might be called often. So keep in mind, we only want to do it once. So we need to declare a boolean that states if we already have added a listener. Also we should check if the DOM is really there because the function is also called a few times before your HTML was injected. Here's the code.
constructor(private renderer: Renderer2, private router: Router){}
#ViewChild('markdownDiv') markdownDiv: ElementRef;
// this is our boolean that prevents us from multiple addings
listenerAdded: boolean = false;
// this hook makes sure the html is already available
ngAfterViewChecked() {
// check if the div we watch has some innerHTML and if we haven't set the link already
if(this.markdownDiv.nativeElement.childNodes.length>0 && !listenerAdded) {
// find a link tag inside your innerHTML
// this can be any selector, like class, id or tag
var link = this.markdownDiv.nativeElement.querySelector("a");
// ... on click
this.renderer.listen(link, 'click', (event) => {
this.router.navigateByUrl("/articles#introduction");
});
// remember not to do it again
this.listenerAdded=true;
}
}
Okay, so I added a click event to the <div></div> holding the TOC:
<mat-sidenav mode="side" #sidenav id="sidenav" fixedInViewport="fixed" fixedTopGap="65">
<button mat-menu-item [routerLink]="['articles']">
<mat-icon svgIcon="arrow-left"></mat-icon>
Return to Article List
</button>
<div class="sidenav-toc" (click)="onTocClick($event)" [innerHtml]="article | MarkdownToc" id="toc" #tocDiv></div>
</mat-sidenav>
Then on my sidenav component I added two functions:
public onTocClick(event: Event) {
const elem = event.target as Element;
if (elem.tagName.toLowerCase() == 'a') {
const frag = elem.getAttribute('data-link');
const id = this.interModuleService.currentArticle.split(' ').join('-');
this.goTo(frag);
}
}
goTo(anchor: string) {
// TODO - HACK: remove click once https://github.com/angular/angular/issues/6595 is fixed
(<HTMLScriptElement>document.querySelector('#'+ anchor)).scrollIntoView();
}
I had thought about some kind of dynamic component. However, due to the issue in Angular, it's not easy to scroll to an anchor either due to the issue above.

Update dom after component rendered the view, best practice in Angular2?

I'm seeking some advice how I should handle elements when working with Angular2.
I have stored some elements id's in the localstorage, and want to set a selected class name on some specific elements.
For now I'm using this way:
ngAfterViewChecked() {
// Check if seats has been selected before
var selectedSeats: elementInterface = JSON.parse(localStorage.getItem('SelectedSeats'));
if (selectedSeats != null) {
var outboundElement = document.getElementById(selectedSeats.outboundSelectedElement);
var returnElement = document.getElementById(selectedSeats.returnSelectedElement);
this.autoSelectSeats(outboundElement);
this.autoSelectSeats(returnElement);
}
}
Method:
private autoSelectSeats(element: Element) {
// Set selected class on element
element.classList.add('selected');
}
I see two issues here. The first is the ngAfterViewChecked hook that fires more than once after the view is created. Is there something I can do so it only fires once?
Second: Is there a better way to get the element when you know the id and set a class attribute on it after the content has loaded?
I can't seem to find the Angular2 way of doing it, besides using this hook.
Any idea? Also, please don't post Jquery posts, as I don't want to implement that when using Angular :)
How about adding a custom directive to each of your "seat" elements and let that directive add the CSS class?
In your template, the directive would be used as follows (I'm guessing, since you didn't show your template):
<div *ngFor="let seat of seats" [highlight]="seat.id">
...
</div>
You need to pass some information to the directive to identify which seat it is working on. It seems better to pass an id directly (e.g. seat.id) rather than to rely on HTML ids (although in your case they might be one and the same).
Now the code for the directive:
#Directive({
selector: '[highlight]'
})
export class HighlightDirective {
#Input() highlight: string; // This will contain a seat.id
constructor(el: ElementRef, ss: SeatService) {
const selectedSeats = ss.getSelectedSeats();
// If current seat found in selectedSeats, mark it as selected.
if (selectedSeats.indexOf(this.highlight) !== -1) {
this.el.nativeElement.classList.add('selected');
}
}
}
The reason I'm using an external service SeatService to get the data from localStorage is that Angular will create an instance of HighlightDirective for every match it finds in your template. You don't want to refetch the selected seats in every instance (the service lets you cache the seats and return the same data).
Angular way has pretty good documentation, classes are toggled using the following syntax: [class.selected]="selected"

How to use templateRef?

I am trying to find a way to dynamically construct a template in Angular2. I was thinking templateRef might provide a way to do this. But I could be wrong.
I found an example of templateRef being used here.
I was looking at templateRef in this example. I noticed the syntax is [ng-for-template] I also tried [ngForTemplate] cause I know this has changed recently.
So at the moment I have this:
import {Component, TemplateRef} from 'angular2/core';
#Component({
selector : 'body',
template : `
<template [ngForTemplate]="container">
<div class="container"></div>
</template>
`
})
export class App
{
#ContentChild(TemplateRef) container;
constructor() {}
ngAfterContentInit()
{
console.log(this);
}
}
This example throws an error:
Can't bind to 'ngForTemplate' since it isn't a known native property
So firstly I am wondering. What is the right way to do this? The docs don't provide any examples.
Secondly, is there a good way I can add new template logic to my template or dynamically construct a template? The structure of the application can be a very large amount of different structural combinations. So if possible I would like to see if there is a way I can do this without having a huge template with a bunch of different ngIf and ngSwitch statements..
My question is really the first part about templateRef. But any help or suggestions on the second part is appreciated.
Creating your own template directive it's not difficult, you have to understand two main things
TemplateRef contains what's inside your <template> tag
ViewContainerRef as commented by Gunter, holds the template's view and will let you to embed what's inside the template into the view itself.
I will use an example I have when I tried to solve this issue, my approach is not the best for that, but it will work for explaining how it works.
I want to clarify too that you can use any attribute for your templates, even if they're already used by builtin directives (obviously this is not a good idea, but you can do it).
Consider my approach for ngIfIn (my poor approach)
<template [ngIfValue]="'make'" [ngIfIn]="obj">
This will print
</template>
<template [ngIfValue]="'notExistingValue'" [ngIfIn]="obj">
This won't print
</template>
We have here two templates using two inputs each ngIfIn and ngIfValue, so I need my directive to grab the template by these two inputs and get their values too, so it would look like this
#Directive({
selector : '[ngIfIn][ngIfValue]',
inputs : ['ngIfIn', 'ngIfValue']
})
First I need to inject the two classes I mentioned above
constructor(private _vr: ViewContainerRef, private _tr: TemplateRef) {}
I also need to cache the values I'm passing through the inputs
_value: any;
_obj: any;
// Value passed through <template [ngIfValue]="'...'">
set ngIfValue(value: any) {
this._value = value;
}
// Value passed through <template [ngIfIn]="...">
set ngIfIn(obj: any) {
this._obj = obj;
}
In my case I depend on these two values, I could have my logic in ngOnInit but that would run once and wouldn't listen for changes in any of the inputs, so I put the logic in ngOnChanges. Remember that ngOnChanges is called right after the data-bound properties have been checked and before view and content children are checked if at least one of them has changed (copy and paste from the docs).
Now I basically copy & paste NgIf logic (not so complex, but similar)
// ngOnChanges so this gets re-evaluated when one of the inputs change its value
ngOnChanges(changes) {
if(this._value in this._obj) {
// If the condition is true, we embed our template content (TemplateRef) into the view
this._vr.createEmbeddedView(this._tr);
} else {
// If the condition is false we remove the content of the view
this._vr.clear();
}
}
As you see it's not that complicated : Grab a TemplateRef, grab a ViewContainerRef, do some logic and embed the TemplateRef in the view using ViewContainerRef.
Hopefully I made myself clear and I made how to use them clear enough also. Here's a plnkr with the example I explained.
ngForTemplate is only supported with ngFor
<template [ngFor] [ngForOf]="..." [ngForTemplate]="container"
or
<div *ngFor="..." [ngForTemplate]="container"
not on a plain template. It is an #Input() on the NgFor directive
Another way to use TemplateRef
If you have a reference to ViewContainerRef you can use it to "stamp" the template
constructor(private _viewContainer: ViewContainerRef) { }
ngOnInit() {
this.childView = this._viewContainer.createEmbeddedView(this.templ);
this.childView.setLocal('data', this.data);
}

I want an action that links me to the Object clicked in a list of Objects

I have a simple action that can be attached to list items in an {{#each}} loop, and when that action is triggered, it will link to that instance of the model.
This is what it looks like now
VpcYeoman.SuperTableController = Ember.ArrayController.extend({
actions: {
goTo: function(input) {
this.transitionToRoute('someModel', input);
}
}
});
The action is called on an HTML element like this
{{action 'goTo' this bubbles=false}}
You can see the problem with this in that 'goTo' cannot be reused on other models because it is specifically looking at the 'someModel' model.
Please help me make this action work for whatever the current model is
I tried replacing 'someModel' with a generic 'model' & even 'this.model' but they didn't work.
Do not reply with 'use {{#link-to}}' please. I am aware that this exists and
Before you read this, you should know that I do recommend you use the link-to helper. I normally pass a computed property to the helper when I need it to change based on the model...
I am not sure where you have that action in your code, but you could just compute that path as needed. For example, take this item controller:
App.ItemController = Ember.ObjectController.extend({
getTransitionPath: function () {
return this.get('foo') + '_bar';
},
transitionPath: function () {
return this.get('foo') + '_bar';
}.property('foo'),
actions: {
goTo: function(input) {
//this.transitionToRoute(this.getTransitionPath(), input); // Regular method
this.transitionToRoute(this.get('transitionPath'), input); // Computed property
}
}
});
I also don't know what kind of logic you are looking for inside of those methods, but this pattern should work on a per model basis.
Good luck!

Categories

Resources