TL;DR: Browser Autofill doesn't work as expected when inputs are in shadow DOMs, particularly noticed with the use of Web Components.
Clarification: The subject of this post is the HTML autocomplete attribute with a custom Web Component input. This is NOT referring to auto-completion of search terms.
Set up: First, let's suppose you want to create a vanilla HTML form to gather a user's name, address, and phone number. You would create a form element with a nested input element for each data point and a submit button. Straightforward and nothing unusual here.
Now, to improve the experience for your users you add the autocomplete attribute to each input with its associated value. I am sure you have seen and used this browser-supported feature before, and if you are like me, it is an expected convenience when filling out online forms for address, credit cards, and username/password.
Up to this point, we don't have any issues--everything is working as expected. With the autocomplete attributes added to the inputs, the browser recognizes that you are trying to fill out a form and a typical browser, such as Chrome, will use whatever user-provided data stored within the browser it can to help auto complete the inputs. In our case, granted you have information stored in your Chrome Preferences/Autofill/'Address and more', you will be given a pop-up list with your stored Address profiles to use to populate the form.
The Twist: If you change your native input to a Web Component with an open shadowDom--because perhaps you want a reusable input that has some validation and styling--the autocomplete no longer works.
Expected result:
I would expect the browser autocomplete feature to work as it normally does, such as, find, associate, and prefill inputs and not discriminate web component inputs that our in shadowDoms.
This is a known, lacking feature which is currently being worked on.
Follow https://groups.google.com/a/chromium.org/g/blink-dev/c/RY9leYMu5hI?pli=1 and https://bugs.chromium.org/p/chromium/issues/detail?id=649162
to stay up to date.
You can work around this by creating your input (and label) outside of the web component and including it via a slot.
const createInput = () => {
const input = document.createElement('input');
input.slot = 'input';
input.className = 'enterCodeInput';
input.name = 'code';
input.id = 'code';
input.autocomplete = 'one-time-code';
input.autocapitalize = 'none';
input.inputMode = 'numeric';
return input;
};
const createLabel = () => {
const label = document.createElement('label');
label.htmlFor = 'code';
label.className = 'enterCodeLabel';
label.innerHTML = `Enter Code`;
return label;
};
#customElement('foo')
class Foo extends LitElement {
#state()
protected _inputEl = createInput();
#state()
protected _labelEl = createLabel();
public connectedCallback() {
this._inputEl.addEventListener('input', this._handleCodeChange);
this.insertAdjacentElement('beforeend', this._labelEl);
this.insertAdjacentElement('beforeend', this._inputEl);
}
public disconnectedCallback() {
this._inputEl?.removeEventListener('input', this._handleCodeChange);
this._labelEl?.remove();
this._inputEl?.remove();
}
public render() {
return html`<form>
<slot name="label"></slot>
<slot name="input"></slot>
</form>`;
}
protected _handleCodeChange = (e: Event) => {
// Do something
};
}
You can style the input and label using the ::slotted pseudo-selector.
css`
::slotted(.enterCodeLabel) {}
::slotted(.enterCodeInput) {}
::slotted(.enterCodeInput:focus) {}
`
Related
On a web page I wish to display an element which depends on the state of some JavaScript. State like in a state machine. Currently the possible states are these (but I may add more):
input: display some input elements for the user to set. The user can click a button to start some JavaScript processing and move to the working state.
working: display a progress bar informing the user that the script is running. The user can cancel the computation (moving back to the input state) or the computation can end (moving to either the result or error state).
result: display the computation result. The user can go back to input with a button.
error: display the error. The user can go back to input with a button.
The JavaScript part is ready and working, but I'm unsure how to do this in HTML + CSS.
Current solution and its issue
Currently I've been doing it with classes: I set a class to a common ancestor element with the same name of the state and I display the right elements based on it. Something like this:
const parent=document.querySelector("#parent");
let timer=null;
function input(){
parent.classList.remove("working","result","error");
parent.classList.add("input");
}
function run(){
parent.classList.remove("input");
parent.classList.add("working");
timer=setTimeout(result,1500)
}
function stop(){
clearTimeout(timer);
input();
}
function result(){
parent.classList.remove("working");
if(Math.random()>0.5){parent.classList.add("result");}
else{parent.classList.add("error");}
}
input();
#input{display:none;}
#working{display:none;}
#result{display:none;}
#error{display:none;}
#parent.input #input{display:block;}
#parent.working #working{display:block;}
#parent.result #result{display:block;}
#parent.error #error{display:block;}
<div id="parent">
<div id="input">INPUT. RUN</div>
<div id="working">WORKING. STOP</div>
<div id="result">RESULT. RESTART</div>
<div id="error">ERROR. RESTART</div>
</div>
This solution works but it feels unstable: in theory it would be possible for the parent element to have no classes (in which case nothing is displayed) or multiple ones (in which case you'd see multiple states at once). This shouldn't happen, but the only thing preventing it is the correctness of my script.
Question
Are there better ways to implement this idea of states, so that the HTML elements can't end up in inconsistent states?
Let’s consider the role which HTML plays in a state machine on the web. A machine has moving parts, it is dynamic, so the core of any machine on the web must be implemented in Javascript. HTML is useful only to provide the interface between the user and the machine. It’s a subtle distinction but it fundamentally changes the way you write it.
Have you ever used React? React provides the framework to create entire web applications as “state machines”. React’s mantra is “UI is a function of state”. In a React app, you have a single variable which contains the current state, rendering code which builds the UI based on the state, and core code (mostly event handlers) which updates the state.
Even if you don’t want to build in React, you can use the same general idea:
keep the current state in a Javascript variable (typically you’d use an object, but in this case we only need a string)
write a rendering function which reads the state and then builds the appropriate HTML to represent that state
in the event handlers for your links, do any operations which are required, update the state and call the rendering function
let state = null
let timer = null
// core code
const input = () => {
state = 'input'
render()
}
const run = () => {
state = 'working'
render()
timer = setTimeout(result,1500)
}
const stop = () => {
clearTimeout(timer)
state = 'input'
render()
}
const result = () => {
if(Math.random()>0.5)
state = 'result'
else
state = 'error'
render()
}
// rendering code
const render = () => {
let x = state
switch(state) {
case 'input':
x += ' run'
break
case 'working':
x += ' stop'
break
case 'result':
x += ' restart'
break
case 'error':
x += ' restart'
break
}
document.getElementById('container').innerHTML = x
}
// initialisation code
state = 'input'
render()
<div id="container"></div>
So basically I have a modal component with an input field that tells it which modal should be opened (coz I didn't want to make a component for each modal):
#Input() type!:string
ngOnChanges(changes: SimpleChanges): void {
this.type = changes["type"].currentValue;
this.openModal();
}
that field is binded to one in the app component:
modalType = "auth";
HTML:
<app-modal [type] = modalType></app-modal>
In the beginning it's got the type "auth" (to login or register), but when I click on an icon I want to open a different modal, I do it like so:
<h1 id="options-route"
(click) ="modalType = 'settings'"
>⚙</h1>
but this only works the first time, when modalType already has the value "settings" the event doesn't trigger even though the value has technically changed
I think the problem is that it's the same value because i tried putting a button that does the exact same thing but with the value "auth" again and with that it was clear that the settings button only worked when tha last modal opened was auth and viceversa
any ideas? I want to be able to open the settings modal more than once consecutively possibly keeping onChange because ngDoCheck gets called a whole lot of times and it slows down the app
You need to include the changeDetectorRef, in order to continue in this way.
More about it https://angular.io/api/core/ChangeDetectorRef
Although, a better and a faster alternative is the use of a behavior Subject.
All you have to do is create a service that makes use of a behavior subject to cycle through each and every value exposed and then retrieve that value in as many components as you want. To do that just check for data changes in the ngOnInit of target component.
You may modify this for implementation,
private headerData = new BehaviorSubject(new HeaderData());
headerDataCurrent = this.headerData.asObservable();
changeHeaderData(headerDataNext : HeaderData) {
this.headerData.next(headerDataNext)
console.log("subscription - changeUserData - "+headerDataNext);
}
Explanation:
HeaderData is a class that includes the various values that can be shared with respective data types.
changeHeaderData({obj: value}), is used to update the subject with multiple values.
headerDataCurrent, an observable has to be subscribed to in the target component and data can be retrieved easily.
I mean i'm too l-a-z-y to use your slightly-not-so-much-tbh complicated answers so I just did this:
I added a counter that tops to 9 then gets resetted to 0 and I add it to the value
screwYouOnChangesImTheMasterAndYouShallDoMyBidding = 0;
//gets called onClick
openSettings(){
if(this.screwYouOnChangesImTheMasterAndYouShallDoMyBidding === 9){
this.screwYouOnChangesImTheMasterAndYouShallDoMyBidding = 0;
}
this.screwYouOnChangesImTheMasterAndYouShallDoMyBidding = this.screwYouOnChangesImTheMasterAndYouShallDoMyBidding + 1;
this.modalType = "settings"+this.screwYouOnChangesImTheMasterAndYouShallDoMyBidding;
}
then in the child component I just cut that last character out:
ngOnChanges(changes: SimpleChanges): void {
let change = changes["type"].currentValue as string;
change = change.substring(0, change.length - 1);
this.type = change;
this.openModal();
}
works like a charm 😂
I have a component A which looks like this
In summary, a user can create different sections/answers and can save them. A rectangular button is created for each saved answer. Internally, all this is saved in Forms and is validated. I am using ace-editor which already provides capability to use the editor as form control.
snippet from A.ts
createForm() {
this.codeEditorForm = this.fb.group({
answer: [null, [this.validateThereIsAtleastOneSavedAnswer(this.answers),this.validateThereIsNoUnsavedAnswer(this.answers)]],
});
}
snippet from A.html
<ace-editor id="editor" class="form-control" formControlName="answer" [ngClass]="validateField('answer')" [(text)]="text"></ace-editor>
I want to use this component as a form control in other components. For eg. I have another component B which also has a form
B.ts
this.bForm = this.fb.group({
field1: [null],
field2: [null],
field3: [null, Validators.required],
field4: [null],
field5: [null], //the value of A maps to this field of the form in B
field6: [null]
},);
}
B.html
<A #a [readonlyFormStatus]="readonlyFormStatus" (answerSectionsEmitter)="handleAEvent($event)" class="form-control" formControlName="field5" [ngClass]="validateField('field5')" ></A>
I want that when bform is submitted only when validation of both bForm and aForm have passed.
What would be the right way to do this following Angular design philosophy?
The correct way seems to be that A implements ControlValueAccessor interface.
export class A implements OnInit, AfterViewInit, ControlValueAccessor {
...
...
}
"There’s the DefaultValueAccessor that takes care of text inputs and textareas, the SelectControlValueAccessor that handles select inputs, or the CheckboxControlValueAccessor, which, surprise, deals with checkboxes, and many more. So for these UI elements, we don't need to create value accessors but for custom components, we need to create a custom accessor" - https://blog.thoughtram.io/angular/2016/07/27/custom-form-controls-in-angular-2.html
Explanation - I am asking formB to take value of A and map it to field5 of formB. But Angular doesn't know what is the value of A. For input fields, Angular already knows that the value of the text box is the value which gets mapped to a form control. But for custom components, we have to explicitly tell Angular what is the value the custom components generates which gets mapped to a form's field. This is done by implementing ControlValueAccess interface.
The interface has 3 important methods.
1) writeValue which is way to tell how the UI changes if the model changes. Say UI of the custom component was a slider with left-end meaning 0% and right-end meaning 100%. If the model changes to say a value say 10/100 then the UI needs to slide to 10%. Update this method to change the UI. In my case, I didn't need to do anything in it because the data input direction in my case is UI to model and not model to UI (my model doesn't create text which needs to be filled in the text region.
writeValue(value:any){
console.log('write value called with value ',value);
}
2) registerOnChange - this is reverse of writeValue. Whenever the UI changes, the model needs to be changed as well. In my case, whenever user writes in textbox then I want to update my model. "Angular provides you with a function and asks you to call it whenever there is a change in your component with the new value so that it can update the control." - https://netbasal.com/angular-custom-form-controls-made-easy-4f963341c8e2
In my case, I want to propogate changes then A's save button is clicked (onSaveAnswer is called then). I want to propogate value of all saved answers at this time
answers:Array<AnswerInformation>;
propagateChange = (_: any) => {};
registerOnChange(fn) {
console.log('registerOnchange called');
this.propagateChange = fn;
}
inSaveAnswer(){
...
this.propagateChange(this.answers);
}
The value that gets propogated gets mapped to the form field to which A is mapped to.
<A #a [readonlyFormStatus]="readonlyFormStatus" (answerSectionsEmitter)="handleAEvent($event)" class="form-control" formControlName="field5" [ngClass]="validateField('field5')" ></A>
field5 will contain the values proporated (this.answers). its structure will be Array<AnswerInformation>; i.e. field5:Array<AnswerInformation>;
I could put addition verification that field5 is not an empty array like so
field5: [null, this.validateField5IsProvided]
validateField5IsProvided(control:AbstractControl) {
const f5:Array<AnswerInformation> = control.value;
if(f5){
if(f5.length !== 0){
// console.log('answer field is valid');
return null;
} else {
return {
validateAnswerIsSaved: { // check the class ShowErrorsComponent to see how validatePassword is used.
valid: false,
message: 'The field can\'t be empty. Please make sure to save the field'
}
};
}
} else {
return {
validateAnswerIsSaved: {
valid: false,
message: 'The fieldcan\'t be empty. Please make sure to save the field'
}
};
}
}
There are couple of more functions that need to be implemented as well
registerOnTouched() {
console.log('registerOnTouched called');
}
setDisabledState(isDisabled: boolean): void {
console.log('set disabled called with value ',isDisabled);
this.editor.setReadOnly(isDisabled);
}
my question is why something like that:
<form id="aaa" action"xxx">
</form>
<button form="aaa" type="submit">Button</button>
does work in Firefox,Opera,Chrome and does NOT work in IE 11 and mobile devices with Windows system? Example above does nothing, button seems not to belongs to form.
thank You in advance.
As already mentioned, the button should ideally be within the form. However, one way to proceed, with the button outside of the form, is to have the button trigger the form submit via JavaScript.
A quick and dirty jQuery example to illustrate:
$('button[form="aaa"]').click(function()
{
$('#aaa').submit();
});
You can replace this with an in-line onclick="" attribute on the button element if preferred.
This question is old but I thought I'd share how I got this working with a React app. I needed the onSubmit callbacks to be run when a form was submitted, and that wasn't happening when calling submit directly on the form. Here was my quick solution to the problem. It only accounts for buttons outside of the form:
/**
* Is the [form] attribute supported?
*/
const isSupported = (() => {
const TEST_FORM_NAME = 'form-attribute-polyfill-test';
const testForm = document.createElement('form');
testForm.setAttribute('id', TEST_FORM_NAME);
testForm.setAttribute('type', 'hidden');
const testInput = document.createElement('input');
testInput.setAttribute('type', 'hidden');
testInput.setAttribute('form', TEST_FORM_NAME);
testForm.appendChild(testInput);
document.body.appendChild(testInput);
document.body.appendChild(testForm);
const sampleElementFound = testForm.elements.length === 1;
document.body.removeChild(testInput);
document.body.removeChild(testForm);
return sampleElementFound;
})();
/**
* If it's not, listen for clicks on buttons with a form attribute.
* In order to submit the form and have all the callbacks run, we insert
* a button, click it and then remove it to simulate a submission from
* inside the form.
*/
if (!isSupported) {
window.addEventListener('click', e => {
const {target} = e;
const formId = target.getAttribute('form');
if (target.nodeName.toLowerCase() === 'button' && formId) {
e.preventDefault();
const form = document.getElementById(formId);
const button = document.createElement('button');
form.appendChild(button);
button.click();
form.removeChild(button);
}
});
}
A submit button is generally used to submit data from a form to the server (JavaScript not taken into account). As the button in your example is not part of the form, it has no associated data to submit.
EDIT: After noticing the form attribute, the user JayC's answer is correct. Internet Explorer does not support that attribute on buttons, whereas the other browsers do. Its a part of the HTML 5 standards which has not been implemented yet.
I am building an UI form via code (not using the UI Builder) and I noticed that the SubmitButton class style is not consistent with the Button class look & feel.
Would you know any way to adjust the look & feel of either the Button class or the SubmitButton class to make them similar.
I noticed that the Button has a call setStylePrimaryName, setStyleName etc... but the documentation is vague - says: "This is useful for debugging"!!!
Any suggestion?
See below screenshoot, first button is of class Button, second button is SubmitButton. You can see they don't even align.
You can style (a button) the way you want with setStyleAttribute
var _btn= {
"background-color":"none",
"background":"none",
"width":"80px",
"height":"24px",
"border":"None",
"font-family":"hobo std",
"font-size":"0.9em",
"color":"3f3f3f",
"opacity":"1",
}
....
....
var closeb = app.createButton("Submit");
library.applyCSS(submit,_btn);
....
....
And in your library you have the function (credits to James Fereira)
function applyCSS(element, style){
for (var key in style){
element.setStyleAttribute(key, style[key]);
}
}
I resolved this cosmetic dilemma by using multiple submit buttons in the same form. I experimented with CSS sans success; the Submit & Reset buttons are two unique beasts in the world of button widgets.
Here is some working code
that demonstrates a multiple page form where each page uses three submitButton's to advance back and forth doing multiple doPost()'s.
// Muliple page form using Google Apps Script
function doGet(eventInfo) {return GUI(eventInfo)};
function doPost(eventInfo) {return GUI(eventInfo)};
function GUI (eventInfo) {
var n = (eventInfo.parameter.state == void(0) ? 0 : parseInt(eventInfo.parameter.state));
var ui = ((n == 0)? UiApp.createApplication() : UiApp.getActiveApplication());
var Form;
switch(n){
case 0: {
Form = getForm(eventInfo,n); // Use identical forms for demo purpose only
} break;
case 1: {
Form = getForm(eventInfo,n); // In reality, each form would differ but...
} break;
default: {
Form = getForm(eventInfo,n) // each form must abide by (implement) the hidden state variable
} break;
}
return ui.add(Form);
};
function getForm(eventInfo,n) {
var ui = UiApp.getActiveApplication();
// Increment the ID stored in a hidden text-box
var state = ui.createTextBox().setId('state').setName('state').setValue(1+n).setVisible(true).setEnabled(false);
var H1 = ui.createHTML("<H1>Form "+n+"</H1>");
var H2 = ui.createHTML(
"<h2>"+(eventInfo.parameter.formId==void(0)?"":"Created by submission of form "+eventInfo.parameter.formId)+"</h2>");
// Add three submit buttons to go forward, backward and to validate the form
var Next = ui.createSubmitButton("Next").setEnabled(true).setVisible(true);
var Back = ui.createSubmitButton("Back").setEnabled(n>1).setVisible(true);
var Validate = ui.createSubmitButton("Validate").setEnabled(n>0).setVisible(true);
var Buttons = ui.createHorizontalPanel().add(Back).add(Validate).add(Next);
var Body = ui.createVerticalPanel().add(H1).add(H2).add(state).add(Buttons).add(getParameters(eventInfo));
var Form = ui.createFormPanel().setId((n>0?'doPost[':'doGet[')+n+']').add(Body);
// Add client handlers using setText() to adjust state prior to form submission
// NB: Use of the .setValue(val) and .setValue(val,bool) methods give runtime errors!
var onClickValidateHandler = ui.createClientHandler().forTargets(state).setText(''+(parseInt(n)));
var onClickBackHandler = ui.createClientHandler().forTargets(state).setText(''+(parseInt(n)-1));
Validate.addClickHandler(onClickValidateHandler);
Back.addClickHandler(onClickBackHandler);
// Add a client handler executed prior to form submission
var onFormSubmit = ui.createClientHandler()
.forTargets(state).setEnabled(true) // Enable so value gets included in post parameters
.forTargets(Body).setStyleAttribute("backgroundColor","#EEE");
Form.addSubmitHandler(onFormSubmit);
return Form;
}
function getParameters(eventInfo) {
var ui = UiApp.getActiveApplication();
var panel = ui.createVerticalPanel().add(ui.createLabel("Parameters: "));
for( p in eventInfo.parameter)
panel.add(ui.createLabel(" - " + p + " = " + eventInfo.parameter[p]));
return panel;
}
The code uses a single "hidden" state (here visualized in a TextBox) and multiple SubmitButton's to allow the user to advance forward and backward through the form sequence, as well as to validate the contents of the form. The two extra SubmitButton's are "rewired" using ClientHandler's that simply modify the hidden state prior to form submission.
Notes
Note the use of the .setText(value) method in the client handler's. Using the Chrome browser I get weird runtime errors if I switch to either of the TextBox's .setValue(value) or .setValue(value, fireEvents) methods.
I tried (unsuccessfully) to implement this logic using a Script Property instead of the hidden TextBox. Instead of client handlers, this requires using server handlers. The behavior is erratic, suggesting to me that the asynchronous server-side events are occurring after the form submission event.