How to catch a return on an input element using Aurelia? - aurelia

In angularjs I had the following:
app.directive('ngEnter', function () {
return function (scope, element, attrs) {
element.bind("keydown keypress", function (event) {
if(event.which === 13) {
scope.$apply(function (){
scope.$eval(attrs.ngEnter);
});
event.preventDefault();
}
});
};
});
And the html was:
<input type="text" ng-model="searchText" class="form-control"
placeholder="Search"
ng-enter="search($event, searchText)">
So basically once I have finished typing my text to search on, when I pressed the enter key the search function on my controller would run.
How would I do this in Aurelia?
I am still learning about its features so any help would be appreciated.

I think an alternative to the angular ngEnter would be:
import {customAttribute, inject} from 'aurelia-framework';
#customAttribute('enter-press')
#inject(Element)
export class EnterPress {
element: Element;
value: Function;
enterPressed: (e: KeyboardEvent) => void;
constructor(element) {
this.element = element;
this.enterPressed = e => {
let key = e.which || e.keyCode;
if (key === 13) {
this.value();//'this' won't be changed so you have access to you VM properties in 'called' method
}
};
}
attached() {
this.element.addEventListener('keypress', this.enterPressed);
}
detached() {
this.element.removeEventListener('keypress', this.enterPressed);
}
}
<input type="password" enter-press.call="signIn()"/>

The simplest way would be to wrap the input in a form element and bind to the submit event of the form.
<form role="form" submit.delegate="search()">
<div class="form-group">
<label for="searchText">Search:</label>
<input type="text" value.bind="searchText"
class="form-control" id="searchText"
placeholder="Search">
</div>
<button type="submit" class="btn btn-default">Submit</button>
</form>
This will give you the same behavior you have built above. I'm still working to create an example of a custom attribute to do this, but honestly, this is how I would recommend you do this specific thing (capturing when the user presses enter).

This does not seem to be supported out of the box. While not perfect, here is what I intend to do:
Add a hidden submit button at the top of my form with a click.delegate attribute.
Run some code in my VM when its clicked. This code will decide what to do with the enter keypress based on any custom logic I need.
Hope that helps,
Andrew
EDIT:
You could also add a keypress event delegate:
<input keypress.delegate="doSomething($event)" />
And define doSomething() as:
doSomething(event) {
if(event.which == 13) {
alert('Your code goes here!');
}
event.preventDefault();
}
This will be a little cleaner when you have many inputs with differing enter keypress behaviours.

Because of keypress.delegate and keypress.trigger in an <input> block entering text by default, your function must return true to avoid it, like this:
doSomething(event) {
if(event.which == 13) {
console.log('Your code goes here!');
event.preventDefault();
return false;
}
return true;
}

Related

Computed Property + Show/Hide DIV

I am not a vue.JS programmer, but have been asked to jump in and make a few changes to an existing app. I am trying to use a computed property to show/hide a DIV.
The problem is the computed property is not being called after the form POST (I put a console.log statement inside the computed method to verify it's not even being called). The findLoginProvider method is invoked successfully and the localStorage items are set... but the DIV is not hidden.
Like I said, I am not a vue.JS programmer and cannot figure out why this isn't working... this is a simplified example of the real page. I really want computed properties to work because I have multiple DIV's to show/hide based on a few computed properties (so manually settings the visibility of a specific DIV is not desired)
HTML:
<div v-if="showFindLoginProvider" :class="{'disabled': isLoading}">
<form #submit.prevent="findLoginProvider" method="POST">
<div>
<label class="block text-gray-700">Email Address</label>
<input
type="email"
name="email"
v-model="email"
placeholder="Enter Email Address"
autofocus
autocomplete
required
/>
</div>
<button type="submit">Continue</button>
</form>
</div>
Methods:
findLoginProvider() {
this.isLoading = true;
UserService
.getLoginProvider(this.email)
.then((response) => {
if (response?.status === 200) {
localStorage.setItem("login_email", this.email);
localStorage.setItem("login_provider", JSON.stringify(response.data));
} else {
this.isError = "Error retrieving login provider";
}});
this.isLoading = false;
},
Computed:
showFindLoginProvider() {
console.log("showFindLoginProvider")
return !this.isLoggedIn && (!this.hasLoginEmail || !this.hasLoginProvider);
},
According to the docs
A computed property will only re-evaluate when some of its reactive dependencies have changed
By saying that in your fnc findLoginProvider you need to change reactive variable to fire showFindLoginProvider run
findLoginProvider() {
this.isLoading = true;
UserService
.getLoginProvider(this.email)
.then((response) => {
if (response?.status === 200) {
// will tell computed to run
this.isLoggedIn = true
// rest of your code
} else {
this.isError = "Error retrieving login provider";
}
});
this.isLoading = false;
},

Vuelidate Two Forms In One Component

I want to validate two whole different forms in a single component. I created the two of them, configured the validations, the methods, data etc. Everything is fine, Moreover, both forms work acutally, but with a little problem. I wrote a little code to explain the problem I was having.
Here is my forms in the component.
<!-- First Form -->
<div>
<form #submit.prevent="submitFormFirst">
<div class="form-group">
<label>Name</label>
<input v-model="name" type="name" class="form-control" placeholder="Name" />
<small v-if="!$v.name.required && $v.name.$dirty">This field is required.</small>
</div>
<!-- Save Button -->
<button type="submit">Save Form 1</button>
</form>
</div>
<!-- Second Form -->
<div>
<form #submit.prevent="submitFormSecond">
<div>
<label>Surname</label>
<input v-model="surname" type="name" placeholder="Surname" />
<small v-if="!$v.surname.required && $v.surname.$dirty">This field is required.</small>
</div>
<!-- Save Button -->
<button type="submit">Save Form 2</button>
</form>
</div>
As you can see there are two different forms and they have their own submit buttons. Here is my data looks like;
data() {
return {
name: null,
surname: null,
};
And my validations...
validations: {
name: {
required,
},
surname: {
required,
}
}
And at the end, I created two methods. One for Form1 and one for Form2.
methods: {
submitFormFirst() {
if (!this.$v.$invalid) {
alert("First Form Submitted.");
let form = {
name: this.name,
};
console.log(form);
} else {
alert("First Form Error.");
}
},
submitFormSecond() {
if (!this.$v.$invalid) {
alert("Second Form Submitted.");
let form = {
surname: this.surname,
};
console.log(form);
} else {
alert("Second Form Error.");
}
},
}
So they have their own methods as well, they are totally seperated from each other, or I do think in this way... The problem is, when the page loads I immadiately go an try to type something in the first form's input and click "Save". It sends the "First Form Error" message with the alert. Then I type something in the second form's input and submit it, then it works, it prints the data in console and sends "Submitted." message. So when I start from the first form, I get an error. When I write and submit the second form, the problem disappears. Then I try the first form again, I see that it works too. The same scenario happens when I start from the second form. In other words, the first form I tried to submit gives an error, then I submit the other form. When I get back to the form I tried first, I see that it works too. What am I missing?
If you want validate two forms you can change method that submit:
methods: {
submitFormFirst() {
this.$v.name.$touch()
if (!this.$v.name.$invalid) {
alert("First Form Submitted.");
let form = {
name: this.name,
};
console.log(form);
} else {
alert("First Form Error.");
}
},
submitFormSecond() {
this.$v.surname.$touch()
if (!this.$v.surname.$invalid) {
alert("Second Form Submitted.");
let form = {
surname: this.surname,
};
console.log(form);
} else {
alert("Second Form Error.");
}
},
}

Using Vee-validate to disable button until form is filled out correctly

I want to disable my submit button until my form is filled out correctly, this is what I have so far:
<form>
<input type="text" class="form-control" v-validate="'required|email'" name="email" placeholder="Email" v-model="userCreate.userPrincipalName" />
<span v-show="errors.has('email')">{{ errors.first('email') }}</span>
<button v-if="errors.any()" disabled="disabled" class="btn btn-primary" v-on:click="sendInvite();" data-dismiss="modal" type="submit">Send Invite</button>
<button v-else="errors.any()" class="btn btn-primary" v-on:click="sendInvite();" data-dismiss="modal" type="submit">Send Invite</button>
</form>
The above only prints an error message and disables my submit button after I've started inputting a value. I need it to be disabled from the start, before I start interacting with the input, so that I cannot send an empty string.
Another question is if there is a better way than using v-ifto do this?
EDIT:
userCreate: {
customerId: null,
userPrincipalName: '',
name: 'unknown',
isAdmin: false,
isGlobalAdmin: false,
parkIds: []
}
Probably simpliest way is to use ValidationObserver slot for a form. Like this:
<ValidationObserver v-slot="{ invalid }">
<form #submit.prevent="submit">
<InputWithValidation rules="required" v-model="first" :error-messages="errors" />
<InputWithValidation rules="required" v-model="second" :error-messages="errors" />
<v-btn :disabled="invalid">Submit</v-btn>
</form>
</ValidationObserver>
More info - Validation Observer
Setting up the button to be :disabled:"errors.any()" disables the button after validation. However, when the component first loads it will still be enabled.
Running this.$validator.validate() in the mounted() method, as #im_tsm suggests, causes the form to validate on startup and immediately show the error messages. That solution will cause the form to look pretty ugly. Also, the Object.keys(this.fields).some(key => this.fields[key].invalid); syntax is super ugly.
Instead, run the validator when the button is clicked, get the validity in the promise, and then use it in a conditional. With this solution, the form looks clean on startup but if they click the button it will show the errors and disable the button.
<button :disabled="errors.any()" v-on:click="sendInvite();">
Send Invite
</button>
sendInvite() {
this.$validator.validate().then(valid=> {
if (valid) {
...
}
})
}
Validator API
One way to disable a button until all the values you need are filled, is to use a computed property that will return bool if all values are assigned or not
Example:
Create a computed property like this:
computed: {
isComplete () {
return this.username && this.password && this.email;
}
}
And bind it to the html disabled attribute as:
<button :disabled='!isComplete'>Send Invite</button
This means, disable the button if !isComplete is true
Also, in your case you don't need two if/else-bound buttons. You can use just one to hide/show it based on if the form is completed or has any errors:
<button :disabled="errors.any() || !isCompleted" class="btn btn-primary" v-on:click="sendInvite();" data-dismiss="modal" type="submit">Send Invite</button>
This button will be disabled until all fields are filled and no errors are found
Another way is to make use of v-validate.initial
<input type="text" class="form-control" v-validate.initial="'required|email'" name="email" placeholder="Email" v-model="userCreate.userPrincipalName" />
This will execute the validation of the email input element after the page is loaded. And makes that your button is disabled before interacting with the input.
To check whether a form is invalid or not we can add a computed property like this:
computed: {
isFormInValid() {
return Object.keys(this.fields).some(key => this.fields[key].invalid);
},
},
Now if you want to start checking immediately before user interaction with any of the fields, you can validate manually inside mounted lifecycle hooks:
mounted() {
this.$validator.validate();
}
or using computed
computed: {
formValidated() {
return Object.keys(this.fields).some(key => this.fields[key].validated) && Object.keys(this.fields).some(key => this.fields[key].valid);
}
}
and use
button :disabled="!formValidated" class="btn btn-primary" v-on:click="sendInvite();" data-dismiss="modal" type="submit">
For the current version 3 (As at the time of writing).
Step 1
Ensure form fields can be watched.
Step 2
Get a reference to the validator instance:
<ValidationObserver ref="validator">.
Step 3
Trigger validation silently whenever the form fields change.
Here's an example:
export default {
data() {
return {
form: {
isValid: false,
fields: {
name: '',
phone: '',
}
}
}
},
watch: {
'form.fields': {
deep: true,
handler: function() {
this.updateFormValidity();
}
}
},
methods: {
async updateFormValidity() {
this.form.isValid = await this.$refs.validator.validate({
silent: true // Validate silently and don't cause observer errors to be updated. We only need true/false. No side effects.
});
},
}
}
<button :disabled="form.isValid">
Submit
</button>
You can add computed properties
...
computed: {
isFormValid () {
return Object.values(this.fields).every(({valid}) => valid)
}
}
...
and it bind to the button:
<button :disabled="!isFormValid" class="btn btn-primary" type="submit">Send Invite</button>
i try this on vee-validate version ^2.0.3

Aurelia Dialog not returning response after using dropdown

I'm using the aurelia-dialog plugin to allow users to generate a set of objects, and want the dialog's response to return the chosen objects.
The workflow is that the list of options is generated from an API call using a promise when the activate() method is called on the dialog. The options are then displayed to the user, and selected from a dropdown. The user then clicks ok and the response should be sent back. Here is the code that is supposed to accomplish it:
this.ds.open({
viewModel: MyModal,
model: {
"title": "Select Objects",
"message": "I hate this modal"
}
}).then(response => {
console.log("closed modal");
console.log(response);
if (!response.wasCancelled) {
console.log('OK');
} else {
console.log('cancelled');
}
console.log(response.output);
});
And then in the modal.js:
import {inject} from 'aurelia-framework';
import {DialogController} from 'aurelia-dialog';
import {ModalAPI} from './../apis/modal-api';
//#inject(ModalAPI, DialogController)
export class MyModal {
static inject = [DialogController, ModalAPI];
constructor(controller, api){
this.controller = controller;
this.api = api;
controller.settings.centerHorizontalOnly = true;
}
activate(args){
this.title = args.title;
this.message = args.message;
this.returnedSet = null;
this.historicSetName = null;
this.reportHist = null;
return this.api.getReportHistory().then(reports => {
this.reportHist = reports;
});
}
selectHistoricReport() {
console.log(this.historicSetName);
if(this.historicSetName == "Select a report...") {
this.returnedSet = null;
} else {
var selectedReport = this.reportHist.filter(x => x.name == this.historicSetName)[0];
this.returnedSet = selectedReport.rsids;
}
console.log(this.returnedSet);
}
ok(returnedSet) {
console.log(returnedSet);
this.controller.ok(returnedSet);
}
}
And then the html:
<template>
<require from="../css/upload-set.css"></require>
<ai-dialog class="selector panel panel-primary">
<ai-dialog-header class="panel-heading">
<button type="button" class="close" click.trigger="controller.cancel()" aria-label="Close"><span aria-hidden="true">×</span></button>
<h4 class="modal-title" id="myModalLabel">${title}</h4>
</ai-dialog-header>
<ai-dialog-body class="panel-body container-fluid">
<div class="row">
<div class="col-sm-6">
<label>Report: </label>
<select value.bind="historicSetName" change.delegate="selectHistoricReport()" class="input-md form-control">
<option ref="historicSetPlaceholder">Select a report...</option>
<option repeat.for="historicReport of reportHist">${historicReport.name}</option>
</select>
</div>
</div>
</ai-dialog-body>
<ai-dialog-footer>
<button click.trigger="controller.cancel()">Cancel</button>
<button click.trigger="ok(returnedSet)">Save</button>
</ai-dialog-footer>
</ai-dialog>
</template>
As long as I don't touch the dropdown, the dialog will return a null (or any other value I initialize returnedSet to). However, as soon as I click on the dropdown, clicking either the Save or Cancel button leads to nothing being returned and the console.log lines at the end of my first code block just get skipped. I also tried removing the click.delegate line from my HTML, but that didn't change anything.
Anyone have any idea why this might be happening?
Also, I found this post(Aurelia Dialog and Handling Button Events) with an extremely similar problem, but can't seem to find any solution in there as to what I should do.
Thanks in advance.

Toggle button class using Directive in Angular

I want to toggle a buttons class depending on the state of a form in Angular. The template is based on Bootstrap.
I've setup a directive called IsDirty.
If the form has the class 'ng-valid', add the class 'btn-success' to the submit button.
Alternatively, if the form is dirty and has the class 'ng-dirty', remove the class 'btn-success' from the submit button.
So far this is what I have but it doesn't work.
var angular = require('angular');
angular.module('myApp')
.directive('isDirty', [function() {
return {
restrict: 'E',
link: function(scope, element, attrs, ctrl) {
var submitButton = element.find('.btn-primary');
if(element.hasClass('ng-valid')) {
submitButton.addClass('btn-success');
} else {
submitButton.removeClass('btn-success');
}
scope.$apply();
}
};
}]);
My form:
<form is-dirty class="form-horizontal" role="form" name="profileForm">
<!-- INPUT FIELDS HERE //-->
<button type="submit" class="btn btn-primary">Save Changes</button>
<button type="reset" class="btn btn-default">Cancel</button>
</form>
This should hopefully fix your problem
.directive('ngDirty', [function() {
return {
restrict: 'AE',
link: function(scope, element, attrs, ctrl) {
var submitButton = element[0].querySelector('.btn-primary');
if(element.hasClass('ng-valid')) {
submitButton.classList.add("btn-danger");
} else {
submitButton.classList.remove("btn-danger");
}
}
};
}]);
Plnkr Example
Update:
It's a little dirty but it seems to work and checks each input, you must bind each input to an ng-model though I have used $scope.input
New Plnkr
2nd Update
I have removed the function and brought in a $timeout you will see from the example how it works.
New update with a $timeout as function
Use ngClass for this (btw, I am confused with your class names. In your description you say add/remove the class .btn-success but in the code you are adding/removing .btn-danger. So in the code below, I am sticking with .btn-success):
<form is-dirty class="form-horizontal" role="form" name="profileForm">
<!-- INPUT FIELDS HERE //-->
<button type="submit"
class="btn btn-primary"
ng-class="{'btn-success' : isValid}">
Save Changes
</button>
<button type="reset" class="btn btn-default">Cancel</button>
</form>
And in your directive:
var angular = require('angular');
angular.module('myApp')
.directive('form', [function() {
return {
restrict: 'EA',
link: function(scope, element, attrs, ctrl) {
scope.isValid = element.hasClass('ng-valid');
}
};
}]);
I would further suggest that you actually make the class ng-valid itself with ng-class and use the variable scope.isValid to change between ng-valid and isDirty.