I have a form-associated customElement which manages its own validity and validation messages. It works as expected as a member of a form; calling form.reportValidity() will place the appropriate error message on the appropriate anchor element. However, calling customElement.reportValidity() results in the error: An invalid form control with name='' is not focusable.
My understanding of reportValidity() is that we can call it on individual elements, whether it is a member of an HTMLFormControlsCollection or not. For example, you can call report validity on the following orphan input:
document.querySelector("#foo").reportValidity();
<input id="foo" value="" required>
So because the above works, I don't believe I'm out of line with any design principle.
My minimal repro sets up a form associted custom element by calling attachInternals and building a shadowDom of a single input element. A single instance of this custom element is nested within a form element. The page loads, and calls reportValidity on both the form and the custom element. The call on the custom element fails:
class FACE extends HTMLElement {
static get formAssociated() { return true; }
constructor() {
super();
this.attachShadow({mode:"open"});
this._internals = this.attachInternals();
}
connectedCallback() {
this.shadowRoot.innerHTML = `<input id="foo" value="initial">`;
let errorAnchor = this.shadowRoot.querySelector("#foo");
this._internals.setValidity({badInput: true}, "Some error message", errorAnchor);
}
reportValidity() { // expose reportValidity on the CE's surface
return this._internals.reportValidity();
}
}
customElements.define("fa-ce", FACE);
customElements.whenDefined("fa-ce").then(
() => {
// reports custom validation message
document.querySelector("form").reportValidity();
// "An invalid form control with name='' is not focusable."
document.querySelector("fa-ce").reportValidity();
}
);
<form>
<fa-ce></fa-ce>
</form>
So (most probable) I'm doing something wrong and you might could help, or this is a bug in (Chrome's) custom elements (FF hasn't implemented attachInternals yet), or this is as designed and what I'm trying to do can't be done.
update There now is a Bug reported:
https://bugs.chromium.org/p/chromium/issues/detail?id=1139621&q=%22not%20focusable%22&can=2
This answer might no longer be valid.
You need to make the Form Element inside shadowDOM focusable
when you create the shadowDOM:
this.attachShadow({
mode: "open",
delegatesFocus: true
});
Does not seem to work in a SO snippet...
working JSFiddle at: https://jsfiddle.net/WebComponents/9uq15ndr/
.
a delegateFocus explanation JSFiddle at: https://jsfiddle.net/WebComponents/jro0n7fz/
as you said: This does not work in FireFox yet (jan 2020)
works in Chrome, Edge, Safari (last one I did not test myself)
Another cause of the error can be a hidden required Field, which the browser can't focus on
<input type="hidden" required />
Related
I might have somewhat a tricky question and just want to be sure, I went the correct way.
To be precise, it's vuelidate 0x on vue 2 with good old boostrap-vue
Let's pressure we have a simple form, with the following validation
<b-form>
.....
<b-form-group
id="title"
label="Title"
label-for="Title"
>
<b-form-input
id="title-input"
v-model="$v.form.title.$model"
:state="validateState('title')"
type="text"
></b-form-input>
<b-form-invalid-feedback id="title-input-live-feedback">
You must enter a title.
</b-form-invalid-feedback>
</b-form-group>
....
validations: {
form: {
title: { required }
}
},
So when we create this item. I do the following:
async createCompany() {
if (!this.$v.$dirty) {
this.$v.$touch()
}
if (this.$v.$invalid) return
try {
await this.storeCompany()
...
} catch (error) {
//
}
},
Now, I come back to the form, for example, to modify it. This means, the item has once passed the validation successfully, especially for required fields.
I noticed that in that sense $touch() is not necessary, unless you add new validations, which would always result in $invalid. Still, I wanted to have a solution that will also work for this case, just so I do not stumble over it in the future.
So, to come up with a "flexible" solution, I did the following:
async updateCompany() {
if (this.$v.$invalid && !this.$v.$dirty) {
this.$v.$touch()
return
}
if (this.$v.$invalid) return
try {
let response = await CompaniesApi.update(this.id, this.form)
...
} catch (error) {
//
}
},
Basically, I first check if $invalid is true and if the form was not submitted (by checking for !this.$v.$dirty).
It works exactly as I want, no double, triple touches and no flickering in terms of validation changes due to touches.
What gives me a bit of a headache is the fact that I first check for this.$v.$invalid before I actually $touch().
Somehow, I thought it must be the other way around.
But I guess I misunderstood $touch() in a way that it's main job is really to show the $errors if needed (vuelidate only shows errors when $invalid and $dirty is set to true).
I know I could go the other way, disabling the submit button until the form is valid, but then I would need to add further elements to the form, e.g. make clear what is missing or maybe even $touch() the validation in the created() or mounted() hook.
Which however is also not what I want, because it would show errors to the user without any interaction.
So, the only solution I could find is the one described.
Did anybody else come across this? I mean, it does what I want, still would appreciate any feedback/ideas.
I implemented a custom control which is basically a styled file uploader specifically for images, which allows the preview of images. The problem is that I experience a weird behavior: the setter of the property to which the regular HTML <input type="file"> is bound gets called twice in FF 53, IE 11, Edge 38, but not in Chrome 57. I have no idea why this happens, perhaps some of you have experienced similar behavior and know a solution.
The markup is the following:
<template>
<!-- markup for preview functionality, irrelevant here -->
<input type="file" class="sr-only" id="${id}" aria-describedby="${id}-help" multiple accept="image/*" files.bind="imageGroup" />
<label class="btn btn-default btn-sm" aria-hidden="true" for="${id}" role="button">
<i class="fa fa-plus" aria-hidden="true"></i> Add files
</label>
</template>
The backing TypeScript code:
import { bindable, bindingMode, autoinject } from 'aurelia-framework';
import { DialogService } from 'aurelia-dialog';
import { Confirm } from '../confirm';
#autoinject
export class ImageUploader {
#bindable({ defaultBindingMode: bindingMode.twoWay }) imageArray: Array<File> = null;
#bindable id: string = null;
#bindable maxImageCount: number = 10;
#bindable maxFileSizeKiloBytes: number = 2048;
constructor(private dialogService: DialogService) { }
idChanged(newValue: string) {
if (!newValue) {
throw Error('The id parameter needs a value in order to make the file uploader work.');
}
}
set imageGroup(fileList: FileList) {
console.log('imageGroup');
if (!fileList || !fileList.length) return;
if (!this.imageArray) return;
for (let i = 0; i < fileList.length; ++i) {
let file = fileList.item(i);
if (this.imageArray.length >= this.maxImageCount) {
// TODO: Alert: maximum number of images reached
} else {
if (file && file.type.startsWith('image/')) {
// Size is in bytes. Div by 1024 to get kilobytes.
if (file.size / (1024) > this.maxFileSizeKiloBytes) {
// TODO: Alert: Image file too large.
} else {
this.imageArray.push(file);
}
} else {
// TODO: Alert: unsupported media type.
}
}
}
}
}
As you can see, I use the "hide the default file uploader and style a bound label instead" trick to style the uploader itself - I only say this to point out that I've checked if maybe this is the cause, but it's not, the same behavior can be experienced if I show the default file uploader and use that.
You can also see that I've bound the <input type="file"> to a setter-only property. I've done that because this property is basically just a proxy which populates another array (which is not in the control, this is what is bound to the control) which is necessary because I need to allow the user to upload files in multiple turns so that (s)he can select files from different folders.
The rest of the code is irrelevant, because the console.log line runs twice whenever I select some files, so the error is located somewhere here - I am just unable to figure out exactly what is causing this to happen.
Any help is appreciated.
I confirmed that using setter on that case causes to call your setter two times in FF, aurelia created another setter for every property that needs to observe. However you can achieve same behavior using observable with minimal changes and also provided encapsulation in this case. See those gist.run link. More info here.
UPDATE
For Firefox this is still open as a bug. But a workaround using change.delegate as elefantino and abemeister said works on FF, see gist here.
Using Durandal 2.0.1 (can't update to 2.1.0 yet due to restrictions on project development) and I have an intermittent issue with the error shown in this question title.
All I'm doing is defining a custom dialog box then showing it:
var pleaseWaitModal = new modalBusy();
dialog.show(pleaseWaitModal);
And when my ajax call is finished I do:
dialog.close(pleaseWaitModal);
...and then display another modal with the results of my ajax call.
This all works perfectly IF the ajax call takes half a second to a second. If it's a quicker call then I get Uncaught TypeError: Cannot read property 'host' of undefined in my console window. The box still closes, it's just that I get a panicky project manager asking what the red error is for...
Is this purely because I'm trying to run "dialog.close()" before "dialog.show()" has properly completed in some circumstances?
The sequence of events is basically:
*user instigates action requiring a detailed modal dialog to appear with data in it
*as it takes several seconds to populate on some occasions, an interim modal dialog is shown with "please wait" in it
*once the ajax request is complete, the "pleasewait" modal is closed and the "detail" modal is shown
*so a bit like:
var pleaseWaitModal = new modalBusy();
dialog.show(pleaseWaitModal);
//set up deferred calls for ajax data and call ...
var deferredAjax = callDataFunction(myparams...);
return deferredAjax.then(function(result) {
dialog.close(pleaseWaitModal);
var detailModal = new detailModal();
detailModal.show(result);
});
So I don't think I can achieve this using the promise on the dialog.show(pleaseWaitModal) call, can I?
Are you using the promise that is returned from the dialog.close function to open your new modal? You might try this:
From your initial dialog:
dialog.show(new modal()).then(function(responseData) {
dialog.show(new pleaseWaitModal(responseData));
});
I think the problem you're running into is async timing related, which is why using the deferred works so well.
EDIT: Related to my comment below, you might look at using only one modal, and putting a loading indicator inside of it, like so:
view.html
<div data-bind="visible: isLoading">
<h1>Please wait...</h1>
<i class="icon-spin icon-spinner icon-4x"></i>
</div>
modalViewModel.js
var vm = {
isLoading: ko.observable(true)
};
vm.activate = function() {
makeAjaxCall().then(function(data) {
vm.isLoading(false);
**Do whatever you need for your ajax return**
return true;
});
});
I think that should work for what you need as an alternative.
I have a SPA in which I am using Durandal / KnockoutJS with knockout.validation.js. Inside the {view}.js I have this set
ko.validation.configure ({
decorateElement: true,
registerExtenders: true,
messagesOnModified: true,
insertMessages: false,
messageTemplate: null,
parseInputAttributes: true,
});
and inside one of the views I have
<input class="input-member"
type="text"
data-bind="value: memberno, validationOptions: { errorElementClass: 'input-validation-error' }"/>
When the view is first activated the element correctly has the style input-validation-error applied.
On subsequent loading of the view the css is not applied as I require. I can see in firebug that the input field now has this class applied input-member validationElement
I don't know where validationElement came from - but something got messed up with the knockout bindings. I have tried moving the validation config into the shell.js but the result is the same (and not a good idea anyway).
Edit:
So far looks like errorElementClass: 'input-validation-error' is not being reapplied to the element after navigation. If the field is modified-focused-cleared , the validation fires normally. validationElement is the placeholder for the errorElementClass
Update
Found this bit of info at the github site and seems to be what im after
in {view}.js
function activate() {
vm.memberno.isModified(false);
return true;
}
The above code seems to work for input fields but not for select fields. Another idea I'm looking at is adding a binding handler like so
ko.bindingHandlers.validationElement = {
update: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
var valueIsValid = valueAccessor().isValid();
if (!valueIsValid) {
$(element).addClass("input-validation-error");
} else {
$(element).removeClass("input-validation-error");
}
}
}
which works for all selects and inputs but is always on. If there's a way to deactivate this binding until the first form submit fires I think that will do it.
There needs to be a way to re-apply the binding or cause the binding to update. Try putting some code in the view model's activate function that forces validation.
<div dojoType="dojo.Dialog" id="alarmCatDialog" bgColor="#FFFFFF" bgOpacity="0.4" toggle="standard">
<div class='dijitInline'>
<input type='input' class='dateWidgetInput' dojoAttachPoint='numberOfDateNode' selected="true">
</div>
how to show this dialog I tried dijit.byId('alarmCatDialog').show();
The above code is a template and I called dijit.byId('alarmCatDialog').show() from the .js file .
dojo.attr(this.numberOfDateNode) this code works and I got the data .but if I change dojoattachpoint to id then I try dijit.byId('numberOfDateNode') will not work;
Your numberOfDateNode is a plain DOM node, not a widget/dijit, i.e. javascript object extending dijit/_Widget, which is the reason you cannot get a reference to it via dijit.byId("numberOfDateNode"). Use dojo.byId("numberOfDateNode") instead and you are all set.
dojoAttachPoint or its HTML5 valid version data-dojo-attach-point is being used inside a dijit template to attach a reference to DOM node or child dijit to dijit javascript object, which is the reason dijit.byId('alarmCatDialog').numberOfDateNode has a reference to your <input type='input' class='dateWidgetInput' .../>.
The main reason to use data-dojo-attach-point is that:
you can create multiple instances of dijit and therefore your template cannot identify nodes/dijits by IDs as you will have multiple nodes/dijits with the same ID
it's an elegant declarative way, so your code won't be full of dijit.byId/dojo.byId.
It is important to keep track of what is the contents and which is the template of the dijit.Dialog. Once you set contents of a dialog, its markup is parsed - but not in a manner, such that the TemplatedMixin is applied to the content-markup-declared-widgets.
To successfully implement a template, you would need something similar to the following code, note that I've commented where attachPoints kicks in.
This SitePen blog renders nice info on the subject
define(
[
"dojo/declare",
"dojo/_base/lang",
"dijit/_Templated",
"dijit/_Widget",
"dijit/Dialog"
], function(
declare,
lang,
_Templated,
_Widget,
Dialog
) {
return declare("my.Dialog", [Dialog, _Templated], {
// set any widget (Dialog construct) default parameters here
toggle: 'standard',
// render the dijit over a specific template
// you should be aware, that once this templateString is overloaded,
// then the one within Dialog is not rendered
templateString: '<div bgColor="#FFFFFF" bgOpacity="0.4">' +// our domNode reference
'<div class="dijitInline">' +
// setting a dojoAttachPoint makes it referencable from within widget by this attribute's value
' <input type="input" class="dateWidgetInput" dojoAttachPoint="numberOfDateNode" selected="true">' +
'</div>' +
'</div>',
constructor: function(args, srcNodeRef) {
args = args || {} // assert, we must mixin minimum an empty object
lang.mixin(this, args);
},
postCreate: function() {
// with most overrides, preferred way is to call super functionality first
this.inherited(arguments);
// here we can manipulate the contents of our widget,
// template parser _has run from this point forward
var input = this.numberOfDateNode;
// say we want to perform something on the numberOfDateNode, do so
},
// say we want to use dojo.Stateful pattern such that a call like
// myDialogInstance.set("dateValue", 1234)
// will automatically set the input.value, do as follows
_setDateValueAttr: function(val) {
// NB: USING dojoAttachPoint REFERENCE
this.numberOfDateNode.value = val;
},
// and in turn we must set up the getter
_getDateValueAttr: function() {
// NB: USING dojoAttachPoint REFERENCE
return this.numberOfDateNode.value;
}
});
});