Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Input bindings don't update when changes are reverted within a setter or changeHandler #442

Open
AdamWillden opened this issue Jun 29, 2016 · 5 comments

Comments

@AdamWillden
Copy link

AdamWillden commented Jun 29, 2016

I think these examples will show the issue clearly enough to void more explanation. The interpolated text shows the real value but the inputs don't revert to the underlying value.

getter/setter example: https://gist.run/?id=8c56fe352fd849462d39311ede2c8b5d
computed getter/setter example: https://gist.run/?id=14ce505a70736ca3c76943ac762fe836
changeHandler example: https://gist.run/?id=45e93cac4e74d8ffb702325d56a7192a

I believe the same can be said for textboxes too. I have alarm limits whose ranges are validated (which is related to this). If the limit is at 100, it cannot exceed 100 and 101 is entered it is reverted to a value of 100 but the input itself doesn't reflect this change.

Any ideas? Is this something that can be fixed in the binding system?

@AdamWillden
Copy link
Author

AdamWillden commented Jul 1, 2016

I implemented a work around by using a binding signaler on the input - I don't consider this an acceptable 'fix' however but I'm posting in case someone comes across the same issue.

Below is code for the numeric limit input. I only signal when the value has been clamped back to the same old value (clamped && limit === oldLimit).

<input value.bind="limitConfig.limit & signal:limitConfig.limitSignal & updateTrigger:'blur'">
  limitChanged(limit, oldLimit) {
    if (typeof limit !== 'number') {
      limit = Number(limit);
      this.limit = limit;
    }

    let clamped = false;

    if (this.enabled) {
      if (this.lowerLimitConfig !== null && this.lowerLimitConfig.upperBound > limit) {
        limit = this.lowerLimitConfig.upperBound;
        clamped = true;
      }

      if (this.upperLimitConfig !== null && this.upperLimitConfig.lowerBound < limit) {
        limit = this.upperLimitConfig.lowerBound;
        clamped = true;
      }
    }

    if (clamped) {
      this.limit = limit;
    }

    if (clamped && limit === oldLimit) {
      this.bindingSignaler.signal(this.limitSignal);
    }
  }

@fopsdev
Copy link

fopsdev commented Jul 2, 2016

A beforeUpdate(newValue,oldValue, cancel) would be nice here
I've opened an issue some while ago with a very similar request:
aurelia/templating-resources#137
The answer is once more: a custom bindingbehaviour :)

It turns out that with Aurelia you can easely generate demo apps and prototypes. But when it comes down to some very usual use cases like converting values (http://stackoverflow.com/questions/37882659/force-view-to-be-updated-from-within-a-valueconverter), or rejecting changes like mentioned here we often need to take a deep plunge and use much more advanced concepts, possibly scaring away people from using this nice framework after they have mastered the demo apps and tutorial phase.

So what can we imo do.
Either those missing pieces in the default concepts in valueConverter and propertyChanged gets added or a set of default bindingbehaviours should be added to the product.

@AdamWillden
Copy link
Author

AdamWillden commented Jul 2, 2016

Thanks for the link to that issue @fopsdev - those advanced techniques are quite well... advanced! I agree this should be a little easier to accomplish as a standard operation that aurelia directly supports.

I think to fix this issue in particular it needn't be something the developer has to handle themselves. I expect the input (value) binding should recheck the value after setting it and then reset the input/checked value appropriately.

@RomkeVdMeulen
Copy link
Contributor

I'm with @AdamWillden on this. I was quite surprised to find that value converters don't support this behavior out of the box. It's a common use case to do some normalisation like trim on an input, and I would like to be able to simply specify this as:

<input value.bind="myProp | trim & validate" />

If I use something like the above now, the normalised value is passed to the model and used for validation - which is exactly what we want. However, some changes applied by trim are reflected back to the screen and others aren't, which results in a very confusing user experience.

For example: if I type some spaces in the input with the trim converter, the first space is ignored (if the model value started with undefined) and the next ones are entered. If I type a character after that, all the preceding spaces suddenly disappear. And if I type any spaces afterwards, they aren't removed even though the value that will eventually be used won't include these spaces.

Right now if I want to normalise and keep my screen and model value in sync, my options are to

These solutions are all doable, but IMO overkill for what should be a simple and common use case.

@RomkeVdMeulen
Copy link
Contributor

RomkeVdMeulen commented Mar 21, 2017

For now I've built this TypeScript base class for binding behaviors that do normalisation. Maybe it will be of use to others.

export abstract class NormalisationBindingBehavior {

	protected abstract normalise(value: any): any;

	private get scopeName() {
		return this.constructor.name;
	}

	bind(binding: any) {
		const normalise = this.normalise.bind(this);
		const updateSource = binding.updateSource.bind(binding);

		binding[`${this.scopeName}UpdateSourceOriginal`] = binding.updateSource;
		binding.updateSource = function(value: any) {
			const normal = normalise(value);
			updateSource(normal);
			if (normal !== value) {
				this.updateTarget(normal);
			}
		};
	}

	unbind(binding: any) {
		binding.updateSource = binding[`${this.scopeName}UpdateSourceOriginal`];
		delete binding[`${this.scopeName}UpdateSourceOriginal`];
	}
}

Example use:

import {NormalisationBindingBehavior} from "./normalisation-binding-behavior";

export class TrimBindingBehavior extends NormalisationBindingBehavior {

	protected normalise(value: string) {
		return value.trim();
	}
}
<template>
  <require from="bindingbehaviors/trim"></require>

  <input value.bind="myVal & updateTrigger:'blur' & trim"/>
</template>

(Note that I add updateTrigger:'blur' so that the user can still add spaces between words - if the input is trimmed after every change it wouldn't be possible to type spaces at all)

EDIT: this doesn't play nicely with the validate binding behavior: see aurelia/validation#423. I've created a hacky workaround for this case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants