I have an Angular 7 application in which I'm trying to handle a text input in ngAfterViewChecked().
The text input is a node in a mat-tree. It's visibility depends on an ngIf condition. If that condition is not met, I display a span instead. Essentially, if the user double clicks on a node in the tree (a span element), it becomes a text input so that the user can edit the text:
<mat-tree [dataSource]="nestedDataSource" [treeControl]="nestedTreeControl">
<mat-tree-node *matTreeNodeDef="let node">
<li>
<span *ngIf="!node.isInput" (dblClick)="nodeDoubleClicked(node)">{{ node.name }}</span>
<input *ngIf="node.isInput" #nodeNameInput type="text" [(ngModel)]="node.name" (blur)="doneEditting(node)" (keypress)="keyPressed($event, node)" />
</li>
</mat-tree-node>
<mat-nested-tree-node *matTreeNodeDef="let node; when: hasNestedChild">
<button mat-icon-button matTreeNodeToggle>
<mat-icon>
{{ nestedTreeControl.isExpanded(node) ? 'expand_more' : 'chevron_right' }}
</mat-icon>
</button>
<span *ngIf="!node.isInput" (dblClick)="nodeDoubleClicked(node)">{{ node.name }}</span>
<input *ngIf="node.isInput" #nodeNameInput type="text" [(ngModel)]="node.name" (blur)="doneEditting(node)" (keypress)="keyPressed($event, node)" />
<ul [class.collapsed]="!nestedTreeControl.isExpanded(node)">
<ng-container matTreeNodeOutlet></ng-container>
</ul>
</mat-nested-tree-node>
</mat-tree>
When the user double clicks on a node, I not only want it to turn into an input text, I want it to gain focus and select the text inside. In order to do this, I have to get the native element and call .focus() and .select() on it. In order to get the native element, I have to use ViewChildren (where the input is tagged with #nodeNameInput as you can see in the code snippet above). And finally, I need to hook into ngAfterViewChecked() in order to be sure that the QueryList of the ViewChildren is ready.
Here is the code for the component:
#ViewChildren('nodeNameInput') nodeNameInputs: QueryList<ElementRef>;
...
ngAfterViewChecked() {
if (this.nodeNameInputs && this.nodeNameInputs.length) {
this.nodeNameInputs.first.nativeElement.focus();
this.nodeNameInputs.first.nativeElement.select();
}
}
I've ensured that there is only ever one node being edited at a time, so it's safe to use first rather than search through nodeNameInputs to find the one to put in focus and select the text.
This seems to work, but there is a problem. It seems like for every key stroke, ngAfterViewChecked() is also called. What this means is that as the user is editing the text for the node, it gets re-selected for every key stroke. This results in the text the user enters being overwritten on every key stroke.
I have a workaround to this problem:
ngAfterViewChecked() {
if (this.nodeNameInputs && this.nodeNameInputs.length) {
this.nodeNameInputs.first.nativeElement.focus();
if (!this.keyStroked) {
this.nodeNameInputs.first.nativeElement.select();
}
}
}
...where keyStroked is set in the keyPressed handler and set to false in the blur handler.
But I'm wondering if there is another hook that can reliably be used to focus the input and select its text while not responding to key strokes. I chose ngAfterViewChecked because a test showed that it was the only hook in which nodeNameInputs was consistently ready every time (i.e. this.nodeNameInputs.length was always 1). But maybe I missed certain hooks.
My workaround seems like a hack. How would you solve this problem?
Create a focus directive and place that on the input you want focused, you wont have to worry about life cycle events.
import { Directive, ElementRef } from '#angular/core';
#Directive({
selector: '[focus]'
})
export class FocusDirective {
constructor(elm: ElementRef) {
elm.nativeElement.focus();
}
}
and use it
<input focus>
https://stackblitz.com/edit/angular-qnjw1s?file=src%2Fapp%2Fapp.component.html
Related
Very new to Angular, and just trying to get a feel for it. I have an input component:
import { Component } from '#angular/core';
#Component({
selector: 'app-input',
templateUrl: './input.component.html',
})
export class InputComponent {
q = '';
queryChange(value: string) {
this.q = value;
}
}
It's html:
<div>
<input #query type="text" (change)="queryChange(query.value)" />
<button>Search</button>
<div>{{ q }}</div>
</div>
When I type into the input, the {{ q }} doesn't update until I click anywhere on the screen, or hit enter. Almost like refocusing. Coming from React I'm confused as to why this happens with the Angular's (change) rather than updating as I type.
My first thought was that maybe because I'm passing the value of the input to queryChange(query.value) instead of passing the event value like I would usually do in React.
I think the problem is about the DOM and not Angular. You should use (input) instead of (change) if you want the event to trigger every time you type.
<input #query type="text" (input)="queryChange(query.value)" />
See this StackBlitz, as well as change and input MDN references. Specifically, MDN says about change:
Unlike the input event, the change event is not necessarily fired for each alteration to an element's value.
I'm trying to create a custom text editor in React, thus I've created a div which has a contentEditable attribute. Now I want to perform some action when the user selects a part of the inputted text. To do that I'm using the select event as onSelect attribute on my div. The problem is that, select event runs not only when selecting the text, but also when I click on the input box, or after any input. How can I prevent it, so that it gets fired only when the text is selected ?
Component:
function EditorBody(props) {
return (
<div className="editor-body">
<div
className="text-section"
contentEditable
role="textbox"
placeholder="Text goes here ..."
onSelect={() => window.alert("You've selected a text")} // Runs after every input, not only when the text is selected
></div>
</div>
);
}
export default EditorBody;
You can change the logic in your onSelect to be able to determine whether or not to execute the selected logic.
onSelect={(event) => {
if (document.getSelection().toString().length > 0) {
// your selection logic
window.alert(document.getSelection().toString());
}
}}
This way the logic will be executed only if the user is selecting something and not on other primary events that might set off the secondary select event (focus, keypress, etc).
Playing aroud with vue with a todo list.
At this stage, I am able to show the list within and with an edit button, the text will be an input so I can edit the todo items. But for example if I have 10 items, and I clicked edit for item 2 and item 4 and item 5, then all these 3 items will change into input.
What I am thinking is if I click on item 4, item 2 will change back to text, so only one item will show input if edit clicked.
I have my template code as below
<td class="col-8">
<!-- if editable is false, show the checkbox and todo item -->
<div class="custom-control custom-checkbox" v-if="!editable">
{{ todo.item }}
</div>
<!-- if editable is true, turn item into an input so user can enter new value -->
<div v-else>
<input ref="editItem" type="text" class="form-control" :value="todo.item" #input="onInput">
</div>
</td>
<td class="col-2">
<button
class="btn btn-sm"
:class="editable ? 'btn-outline-success' : 'btn-outline-info'"
#click.prevent="editable ? onSave() : onEdit()">{{ editable ? 'save' : 'edit' }}
</button>
</td>
As I open up the vue inspect in chrome, I can see all items have their different data value.
How can I change others siblings' value?
Thanks in advance for any sugguestins and advices.
I wouldn't recommend you to change the value of another sibling, since you cannot be sure of the side-effects it can cause, obviously when you look at a todo list, it would feel like there are no side-effects but in general practice, a node should change elements that are below it in the parentage, neither above nor the siblings.
If you want to change elements above, its always drive via events. You let the parent know that something needs to be changed rather than changing it yourself.
This is what you can do here:
Rather using editable as a data property, use it as a prop to the TodoListItem component.
props: {
editable: {
type: Boolean,
default: false
},
}
When the Save/Edit button is clicked in the child component, rather than changing the value there itself, use events like so.
/** You should keep a deepCopy of the props if you are changing the state
of that object and then send the updated local data prop to the parent
as well for updation rather than changing the prop object.
NEVER CHANGE THE PROP OBJECT DIRECTLY. -> this.todoItem =
this.deepCopy(this.todo);
Do this in before mount hook of the child component.
**/
this.$emit('todo-item-action', action, todoItem);
Once this event is emitted, in the parent TodoList component, catch this event and change the state of the items like so:
<todo-list-item
v-for="todo in todos"
:key="todo['id']"
:todo="todo"
:editable="todo['editable']"
#todo-item-action="handleItemAction"
>
or if you don't want to mutate the todo object. here you can make sure editableItem is null if none is selected or only the active item ID is referred so that the prop will change and automatically other list item will become un-editable.
<todo-list-item
v-for="todo in todos"
:key="todo['id']"
:todo="todo"
:editable="editableItem === todo['id']"
#todo-item-action="handleItemAction"
>
The more you drive a component via events and props, the more re-usable it becomes. Always have an orchestrator component which manipulates these stateless components by handling events propogated by them and sending state via props.
This is the approach I have always followed and it always allow me to refactor cleanly and quickly.
Regards.
I am using angular 8.
I have a textbox, which consists of some text. I want the user to select the text, Once the selection is over, I want to call a function with the selected text as a parameter.
Here is the selected textbox.
Now, when the reaction is selected, I want to call a function. Is there any way, I can do this in Angular 8.
Assuming your textbox is a HTML textarea, you define the select event handler as follows in your template (here it points to the onTextSelected method from your component class).
<textarea #textbox (select)="onTextSelected()">{{ text }}</textarea>
In your component class, you should get a reference to the textare by the use of the #ViewChild decorator. Then you need to implement the onTextSelected method.
#ViewChild('textbox', { static: false}) textAreaRef: ElementRef<HTMLTextAreaElement>;
onTextSelected(): void {
const textArea = this.textAreaRef.nativeElement;
this.selectedText = textArea.value.substring(textArea.selectionStart, textArea.selectionEnd);
}
Please have a look at the following StackBlitz.
In case you really want to call the method with the selected text, you can change your template as follows and get rid of #ViewChild at the same time.
<textarea #textbox (select)="onTextSelected(textbox.value.substring(textbox.selectionStart, textbox.selectionEnd))">{{ text }}</textarea>
This is is shown in the following StackBlitz
Goal:
I want to make an editable table cell component. If the user double clicks on the cell, it's content transforms to an input. If he/she click outside the input, or press enter, the cell returns to it's original state.
What I have so far:
I made a simple version, which is a good starting point. The component has an 'edit' state. If it false, the component shows the value. If true, the component shows an input box, and passes
the value
the component itself, as parent
a function, which will be run on 'mouseLeave' event
http://jsbin.com/cugatedizu/1/edit
You can use a combination of the focusOut event and a observer.
Ur template will look like
{{#if edit}}
{{input value=value focusOut=test parent=this}}
{{else}}
{{value}}
{{/if}}
Add this observer to your component
setInputFocus: function() {
if(this.get('edit')) {
Em.run.scheduleOnce('afterRender', this, function() {
this.$().find('input').focus();
});
}
}.observes('edit')
Working demo.
A more flexible way to write this would be:
{{input value=value parent=this}} //(removed mouseLeave=test)
Then in your component.js you can do:
test: function(){
console.log('mouse out');
this.get('parent').set('edit', false);
}.on('focusOut', 'mouseLeave')
In this way you can be more flexible in how you handle events. In the code above, the cell becomes uneditable if the user hits the tab key, and the mouseLeave behaviour is preserved.
Demo
This editable table widget may work for you too, although it transforms the entire table: http://mindmup.github.io/editable-table/