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/
Related
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
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
cy.on('tap', 'edge,:selected', function (event) {
$scope.$apply(function (response) {
$scope.dataToDisplay = {
data1:true
}
});
});
I have the following code above. It is an event that fires as any edge is clicked/tapped on in a Cytoscape.js graph.
I am able to display the $scope object utilizing $scope.$apply. In the html, I also have an ng-class directive that will display accordingly whether it's true/false. However, the ng-class does not seem to respond as the value changes.
Below is a snippet of the ng-class directive,
<div ng-if="dataToDisplay">
<div class="panel" ng-class="{'panel-danger': dataToDisplay.data1, 'panel-default': !dataToDisplay.data1}">
<div>
</div>
Assistance is much appreciated.
I would like to clarify that the class does get updated as the page loads. However, I have an additional ng-click event that toggles the true/false. This does not add/remove the class as the value change but the value does get updated in the view. How can I achieve update the class as the value changes?
I have an input in my view:
<label for="map-latitude_input">Latitude {{mapLatitudeInput}}</label>
<input
type="text"
placeholder="00.000"
[(ngModel)]="mapLatitudeInput"
[ngFormControl]="newListingForm.find('mapLatitudeInput')"
id="map-latitude_input"
class="transparent">
and inside my controller I am listening to map drag event from google maps api. On drag I want to update value inside my input and in its associated label. At the moment this looks like this:
//Update coordinates on map drag
map.addListener('drag', (event) => {
this.mapLatitudeInput = map.getCenter().lat();
console.log(this.mapLatitudeInput);
});
Now when I drag a map console keeps outputting correct value as I move it around, however in my view it stays exactly the same. What I also noticed is that if I drag map around and than do some action like select an option in select menu within same form that my input is in, it updates mapLatitudeInput to correct value, i'm not sure why this happens, but thought I'd mention it.
It's perhaps due to ZoneJS. This question could give you some hints: View is not updated on change in Angular2.
From where do you get your map instance. If it's instantiated outside a zone, Angular2 won't be able to detect updates of the mapLatitudeInput attribute.
You could try something like that:
export class MapComponent {
constructor(ngZone:NgZone) {
this.ngZone = ngZone;
}
map.addListener('drag', (event) => {
this.ngZone.run(() =>
this.mapLatitudeInput = map.getCenter().lat();
console.log(this.mapLatitudeInput);
});
});
This question could be also related to your problem: Angular2 child property change not firing update on bound property.
Hope it helps you,
Thierry
I've got this helper and event map with a Session. The event is listening for a button click then grabs the input text from the input field. Then it sets the Session. The helper gets the Session and does a find() on the collection for CampYear (one of the collection fields).
Session.setDefault('keyCampYear', '');
Template.camp.helpers({
'Query': function () {
var cy = Session.get('keyCampYear');
return Programs.find({CampYear: cy}).fetch();
}
});
Template.query.events({
'click #camp-year-button': function(event) {
var campYearTemp = document.getElementById('camp-year').value;
Session.set('keyCampYear', campYearTemp);
}
});
The output is set up with a spacebars {{#each}} to present a table:
<tbody>
{{#each Query}}
<tr>
<td>{{Fname}}</td>
<td>{{Lname}}</td>
<td>{{CampYear}}</td>
</tr>
{{/each}}
</tbody>
This works as expected. I now need to add another text field and button to query the database for a different field, and output to the same spacebars {{#each}}. But I'm not able to get it working. Adding another event map, Session and helper doesn't seem to work. Is there a way I can get another input field with its button to search for a different field in the collection and output on the same spacebars {{#each}}?
NON-WORKING CODE BELOW:
Add text box and button:
<input type="text" id="donate">
<button id="donate-button">GO</button>
Add new event map, helper and Session:
Session.setDefault('keyDonate', '');
Template.camp.helpers({
'Query': function () {
var don = Session.get('keyDonate');
return Programs.find({DONATE: don}).fetch();
}
});
Template.query.events({
'click #donate-button': function() {
var donateTemp = parseInt(document.getElementById('donate').value, 10);
Session.set('keyDonate', donateTemp);
}
});
What ends up happening is that the entire Mongo collection loads in the browser by default, without the user clicking either button. The first button and text box are inoperable, the newly created button and text box work, but very sluggishly.
Looks like I found a solution. I'm not familiar with Meteor enough to know that this is THE solution, but here's what I did. First, to prevent the entire collection from loading on the page, both Sessions had to be set to a default of undefined:
Session.setDefault('key', undefined);
Rather than:
Session.setDefault('key', '');
I could default one Session like this with a '' value and it didn't seem to mind, with two set like this, things went haywire. The next change was to add another Spacebars {{#each}} inside the table just below the first one.
{{#each Query}}
<tr>
<td>{{Fname}}</td>
<td>{{Lname}}</td>
<td>{{CampYear}}</td>
</tr>
{{/each}}
{{#each Query2}}
<tr>
<td>{{Fname}}</td>
<td>{{Lname}}</td>
<td>{{CampYear}}</td>
</tr>
{{/each}}
Each helper now needs to link to the appropriate {{#each}}. The third part was that I was forced to set one Session back to undefined while setting the other to prevent stacking the tables on top of each other.
Template.query.events({
'click #donate-button': function() {
var donateTemp = parseInt(document.getElementById('donate').value, 10);
Session.set('keyCampYear', undefined);
Session.set('keyDonate', donateTemp);
}
});
Template.query.events({
'click #camp-year-button': function() {
var campYearTemp = document.getElementById('camp-year').value;
Session.set('keyDonate', undefined);
Session.set('keyCampYear', campYearTemp);
}
});