input type=reset and knockout - forms

Knockout doesn't update observables when a form reset button is clicked.
http://jsfiddle.net/nQXeM/
HTML:
<form>
<input type="text" data-bind="value: test" />
<input type="reset" value="reset" />
</form>
<p data-bind="text: test"></p>
JS:
function ViewModel() {
this.test = ko.observable("");
}
ko.applyBindings(new ViewModel());
Clearly the change event of the input box isn't being fired, as seen with this jQuery test:
http://jsfiddle.net/LK8sM/4/
How would we go about forcing all observables bound to form inputs to update without having to manually specify them if the reset button isn't firing of change events?
It would be easy enough to use jQuery to find all inputs inside the form and trigger change events, but lets assume we've a knockout only controlled form.

I copied and modified the default Knockout submit binding in order to create a similar binding for the form reset event:
ko.bindingHandlers['reset'] = {
init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
if (typeof valueAccessor() !== 'function')
throw new Error('The value for a reset binding must be a function');
ko.utils.registerEventHandler(element, 'reset', function (event) {
var handlerReturnValue;
var value = valueAccessor();
try {
handlerReturnValue = value.call(bindingContext['$data'], element);
} finally {
if (handlerReturnValue !== true) {
if (event.preventDefault)
event.preventDefault();
else
event.returnValue = false;
}
}
});
}
};
You'd bind this like:
<form data-bind="reset: onFormReset">
and onFormReset would be on your view model:
function ViewModel() {
this.onFormReset = function () {
//Your custom logic to notify or reset your specific fields.
return true;
}
}
In your reset handler, if you return true, then JavaScript will continue to call its reset function on the form. If you are setting observables that are bound to value, though, you don't really need to have JavaScript continue to reset the form. Therefore, you could technically not return anything, or return false in that scenario.
Someone else could extend this further to notify all the bound observables in the form automatically, but this worked for my purposes.

As you mentioned, the change event isn't fired when a form is reset. If you're only using KnockOut, I don't think you really have may options unless you create custom bindings that can register for the reset event and detect changes - that would still involve manual JS, but at least it would be centralized.
A more general approach, although it does require jQuery, is to create a function to handle the form's reset event, and detect changes on the form inputs at that time.
Here's an example of an event handler that might work. Please be aware, this is not production-ready code. I would look at it with a good jQuery eye before using :)
$('form').on('reset', function (evt) {
evt.preventDefault();
$(this).find('input, select, textarea').each(function () {
if ($(this).is('input[type="radio"], input[type="checkbox"]')) {
if ($(this).is(':checked') !== $(this)[0].defaultChecked) {
$(this).val($(this)[0].defaultChecked);
$(this).trigger('click');
$(this).trigger('change');
}
} else {
if ($(this).val() !== $(this)[0].defaultValue) {
$(this).val($(this)[0].defaultValue);
$(this).change();
}
}
});
});
Here's a fiddle that demonstrates the idea: http://jsfiddle.net/Fm8rM/2/

Related

How can I safely manipulate DOM in a StencilJS component?

I'm trying to safely remove a DOM node from a component made whit StencilJS.
I've put the removing code in a public method - It's what I need.
But, depending on which moment this method is called, I have a problem. If it is called too early, it don't have the DOM node reference yet - it's undefined.
The code below shows the component code (using StencilJS) and the HTML page.
Calling alert.dismiss() in page script is problematic. Calling the same method clicking the button works fine.
There is a safe way to do this remove()? Do StencilJS provide some resource, something I should test or I should wait?
import {
Component,
Element,
h,
Method
} from '#stencil/core';
#Component({
tag: 'my-alert',
scoped: true
})
export class Alert {
// Reference to dismiss button
dismissButton: HTMLButtonElement;
/**
* StencilJS lifecycle methods
*/
componentDidLoad() {
// Dismiss button click handler
this.dismissButton.addEventListener('click', () => this.dismiss());
}
// If this method is called from "click" event (handler above), everything is ok.
// If it is called from a script executed on the page, this.dismissButton may be undefined.
#Method()
async dismiss() {
// Remove button from DOM
// ** But this.dismissButton is undefined before `render` **
this.dismissButton.remove();
}
render() {
return (
<div>
<slot/>
<button ref={el => this.dismissButton = el as HTMLButtonElement} >
Dismiss
</button>
</div>
);
}
}
<!DOCTYPE html>
<html lang="pt-br">
<head>
<title>App</title>
</head>
<body>
<my-alert>Can be dismissed.</my-alert>
<script type="module">
import { defineCustomElements } from './node_modules/my-alert/alert.js';
defineCustomElements();
(async () => {
await customElements.whenDefined('my-alert');
let alert = document.querySelector('my-alert');
// ** Throw an error, because `this.dismissButton`
// is undefined at this moment.
await alert.dismiss();
})();
</script>
</body>
</html>
There are multiple ways to delete DOM nodes in Stencil.
The simplest is to just call remove() on the element, like any other element:
document.querySelector('my-alert').remove();
Another would be to have a parent container that manages the my-alert message(s). This is especially useful for things like notifications.
#Component({...})
class MyAlertManager {
#Prop({ mutable: true }) alerts = ['alert 1'];
removeAlert(alert: string) {
const index = this.alerts.indexOf(alert);
this.alerts = [
...this.alerts.slice(0, index),
...this.alerts.slice(index + 1, 0),
];
}
render() {
return (
<Host>
{this.alerts.map(alert => <my-alert text={alert} />)}
</Host>
);
}
}
There are other options and which one to choose will depend on the exact use case.
Update
In your specific case I would just render the dismiss button conditionally:
export class Alert {
#State() shouldRenderDismissButton = true;
#Method()
async dismiss() {
this.shouldRenderDismissButton = false;
}
render() {
return (
<div>
<slot/>
{this.shouldRenderDismissButton && <button onClick={() => this.dismiss()}>
Dismiss
</button>
</div>
);
}
}
Generally I would not recommend manually manipulating the DOM in Stencil components directly since that could lead to problems with the next renders since the virtual DOM is out of sync with the real DOM.
And if you really need to wait for the component to render you can use a Promise:
class Alert {
loadPromiseResolve;
loadPromise = new Promise(resolve => this.loadPromiseResolve = resolve);
#Method()
async dismiss() {
// Wait for load
await this.loadPromise;
// Remove button from DOM
this.dismissButton.remove();
}
componentDidLoad() {
this.loadPromiseResolve();
}
}
I previously asked a question about waiting for the next render which would make this a bit cleaner but I don't think it's easily possible at the moment. I might create a feature request for this in the future.

Prevent closing browser tab when form is dirty in Angular 2

How to prevent closing browser tab when form is dirty in Angular 2?
My html body contains a component:
<body>
<my-app>Loading, please wait...</my-app>
</body>
which contains a router navigation and a router outlet:
<nav>
(...)
</nav>
<router-outlet></router-outlet>
and when the router navigates to the edit page, I have some form there:
<form #myForm="ngForm">
<button pButton type="text" label="Save" (click)="onSave()" [disabled]="!myForm.valid || myForm.pristine"></button>
</form>
Now, if the form is not 'pristine', I want to ask for confirmation when the user tries to close the browser tab:
window.onbeforeunload = function() {
if (form.dirty) {
return "You have unsaved data changes. Are you sure to close the page?"
}
}
How can I access the dirty state of Angular form in canonical way from there? I could register an event to field change on each field and set the global dirty flag, but I'd have to put that code on every from and by every navigation and then maintain that code so that the message stays consistent. Is there any other way to check out if there's an angular form on the page, which is in dirty state?
Perhaps
#HostListener('window:beforeunload', ['$event'])
handleBeforeUnload(event) {
if (connected) {
return "You have unsaved data changes. Are you sure to close the page?"
}
}
Add a Hostlistener decorator. If there are unsaved changes on the form confirm dialog appears.
#HostListener('window:beforeunload', ['$event'])
handleBeforeUnload(event: Event) {
event.returnValue = false;
}
This works. Implement the hasUnsavedData() function accordingly.
hasUnsavedData(){
return this.myForm.dirty;
}
#HostListener('window:beforeunload', ['$event'])
handleBeforeUnload($event: any) {
if (this.hasUnsavedData()) {
$event.returnValue = true;
}
}
Simply you can use Jquery to get state of ng-form.
#HostListener('window:beforeunload', ['$event'])
beforeUnloadHandler(event) {
if($('form').hasClass('ng-touched')) { //You can check with ng-dirty based on your requirements.
let confirmMessage = 'You have unsaved data changes. Are you sure to close the page?'
event.returnValue = confirmMessage;
return confirmMessage;
}
}
In my case am just showing warning dialog if that the form has been touched.
Try this directive https://github.com/extremeprog-com/ng-prevent-navigation.
So it should be simple
<div ng-prevent-navigation="vm.pageShouldBeReloaded"
ng-prevent-navigation-text="Payment form has unsaved changes.
If you leave the page now you will lose those changes."
></div>

cannot choose options confirm with bootbox

i have a button that i use to delete records
every time bootbox shows the confirm its closes automatically
here in the example the "cancel" button does not word
where is the mistake?
function ConfermaCancella()
{
// e.preventDefault();
bootbox.confirm("Sure to delete?", function (result) {
if (result) {
return true;
} else {
return false;
}
});
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script src="http://bootboxjs.com/bootbox.js"></script>
<input type="button" onclick = "return ConfermaCancella();" ID="ImageButton3" value="Delete" />
As noted in the documentation:
All Bootstrap modals, unlike native alerts, confirms, or prompts,
generate non-blocking events. Because of this limitation, code that
should not be evaluated until a user has dismissed your dialog should
be placed (or called) within the callback function of the dialog.
So, if you want something to happen only if the user confirms the action, you need to move your code into a callback, like so:
function ConfermaCancella(){
bootbox.confirm('Confirm delete?', function(result){
/* 'result' is a truthy value */
if(result){
/* Do your delete action, probably using AJAX actions */
}
});
return false;
}

backgrid.js - how to prevent multi-row selection?

I am new to backgrid and using it in a form to allow the user to select a row (via a checkbox) and then click "Submit". I cannot figure out how to configure my grid to behave like "radio buttons" in that only one row can be selected. Is this something backgrid natively supports or do I need to write a handler to "unselect" previously selected rows?
Here is a quick-n-dirty method:
wellCollection.on('backgrid:selected', function(model, selected) {
if (wellGrid.getSelectedModels().length > 1) {
model.trigger("backgrid:select", model, false);
alert('Only one selection is allowed.');
}
});
The downside is this approach requires the use of "SelectAll" which is really counter-intuitive to the user. I would prefer to be able to not use "SelectAll" but it is required to get the getSelectedModels object populated.
You could create a custom cell that renders radio buttons. The implementation below may need some more work but something like this will get you started:
var RadioCell = Backgrid.Cell.extend({
events: {
"click .selectedRadio": "makeSelected"
},
render: function () {
this.template = Mustache.to_html($("#radioCell").html(), this.model.toJSON());
this.$el.html(this.template);
this.delegateEvents();
return this;
},
makeSelected: function () {
// set all to inactive
this.model.collection.invoke('set', { "SomeModelAttrib": false });
// check if radio is checked and set the value on the model
var radioValue = $("input[name='someSelection']").is(":checked");
this.model.set("SomeModelAttrib", radioValue);
}
});
and the mustache template:
<script type="text/template" id="radioCell">
<input type='radio' class='selectedRadio' name='someSelection' value="{{SomeModelAttrib}}" {{#SomeModelAttrib}}checked{{/SomeModelAttrib}}>
</script>

mixpanel track form submission with validation

I have a form which I want to use mixpanel to track some properties when I submit. How can I stop the form submit through mixpanel if the validation return false ?
Here's my code in general.
My simple form
<form id="form" action="..." method="post" role="form">
// my elements here
<input type="submit" value="Submit" />
</form>
My script
<script>
function(){
mixpanel.track_forms("form", "MyEventName", getProperties());
$("form").submit(SubmitForm);
function getProperties(){
// get properties here
}
function SubmitForm() {
if (SomethingNotRight()) { return false; }
return true;
}
}
My problem:
I expect that in my SubmitForm function, after validation by SomethingNotRight function, it will stop the submit. However, even when SubmitForm returns false, the form keep submitting to the server, which I found out is because of the mixpanel.track_form.
The reason I use mixpanel.track_form is to avoid the race condition between form submit and mixpanel submit as debugging mixpanel track form
I can definitely understand the issue here, and the reason is that track_forms is just designed for the default use case of a form submitting right away. If you have a process in between (in this case a validation), you should basically do your own implementation. The idea of track_forms is to identify the form being submitted, log the event, wait for a while so that the event can be saved, and then proceed. In that sense, you can do:
(function(){})(
var theForm = $("#form"),
readyToProceed = false;
//listen for the submition
theForm.submit(function(e){
if(!readyToProceed){
e.preventDefault();
processSubmit();
}
});
function processSubmit(){
//validation process
if (SomethingNotRight()) { return false; }
//we are all good, lets proceed
mixpanel.track("Form submitted");
readyToProceed = true;
window.setTimeout(function(){ theForm.submit() }, 300);
}
);