<!DOCTYPE html>
<html>
<head>
<link href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.2/css/bootstrap-combined.min.css" rel="stylesheet" />
<link href="style.css" rel="stylesheet" />
</head>
<body data-ng-app="app" data-ng-controller="personEditController as vm">
<div class="container-fluid">
<h2>Normal form filling</h2>
<p>There are no issues with normal Angular binding when keystrokes flow
through to the model. Alter the name input and everything behaves as expected.
</p>
<p>
<input data-ng-model="vm.person.firstName" placeholder="FirstName" />
<br />
<input data-ng-model="vm.person.lastName" placeholder="LastName" />
<br />
<br />
The full name is {{vm.person.firstName}} {{vm.person.lastName}}
</p>
<h2>The trouble starts here</h2>
<p>
<strong>Try to enter 250.05 in these text boxes.</strong>
</p>
<p>
<label>Without zFloat: </label>
<input data-ng-model="vm.person.balance" placeholder="Balance" />
<br />
</p>
<p>
<label>With zFloat: </label>
<input data-ng-model="vm.person.balance" z-float placeholder="Balance" />
<br />
</p>
<p>
The current balance is {{vm.person.balance}}
</p>
</div>
<p><strong>N.B.: JavaScript doesn't understand ',' in numbers and
only tolerates English formatting of decimals.</strong></p>
<!-- 3rd party libraries -->
<script data-require="angular.js@*" data-semver="1.3.0" src="//code.angularjs.org/1.3.0/angular.js"></script>
<script src="https://cdn.rawgit.com/Breeze/breeze.js/master/build/breeze.debug.js"></script>
<script src="https://cdn.rawgit.com/Breeze/breeze.js.labs/master/breeze.directives.js"></script>
<!-- application scripts -->
<script src="app.js"></script>
<script src="datacontext.js"></script>
<script src="model.js"></script>
<script src="personEditController.js"></script>
</body>
</html>
input {
border: 1px solid #ccc;
font-family: "Segoe UI", Arial, Helvetica, sans-serif;
padding: 0.5em;
margin-top: 0.5em;
width: 300px;
}
.notWorking {
color: red;
font-style: italic;
}
.ngCloak {
display: none;
}
body {
-webkit-font-smoothing: antialiased;
}
input ~ .icon-asterisk-invalid {
top: 0.2em; /* adjust position of 'required' asterisk */
}
footer {
margin-top: 1em;
color: gray;
}
(function () {
'use strict';
angular.module('app').factory('model', model);
function model() {
// aliases
var DT = breeze.DataType;
var Validator = breeze.Validator;
return {
configureMetadataStore: configureMetadataStore
};
function configureMetadataStore(metadataStore) {
addPersonType(metadataStore);
}
function addPersonType(metadataStore) {
metadataStore.addEntityType({
shortName: "Person",
namespace: "Demo",
autoGeneratedKeyType: breeze.AutoGeneratedKeyType.Identity,
dataProperties: {
id: { dataType: DT.Int32, isPartOfKey: true },
firstName: { dataType: DT.String,
validators:[
Validator.required(),
Validator.maxLength({maxLength: 20})] },
lastName: { dataType: DT.String,
validators:[
Validator.required(),
Validator.maxLength({maxLength: 30})] },
balance: {dataType: DT.Double ,
validators:[ Validator.number()]},
}
});
}
}
})();
(function() {
'use strict';
angular.module('app', ['breeze.directives']);
})();
(function() {
'use strict';
angular.module('app').factory('datacontext', ['model', datacontext]);
function datacontext(model) {
var dataService = new breeze.DataService({
serviceName: 'demo',
hasServerMetadata: false
});
var store = new breeze.MetadataStore();
model.configureMetadataStore(store);
var manager = new breeze.EntityManager({
dataService: dataService,
metadataStore: store
});
var service = {
getPersonById: getPersonById
};
return service;
function getPersonById(id){
// fake an existing person in cache
return manager.createEntity('Person',
{id: id,
firstName: "Tracy",
lastName: "Nobody",
balance: 250},
breeze.EntityState.Unchanged); // loaded 'Unchanged' as if queried
}
}
})();
(function() {
'use strict';
angular.module('app')
.controller('personEditController', ['datacontext', controller]);
function controller(datacontext) {
var person42 = datacontext.getPersonById(42);
// ViewModel API
var vm = this;
vm.person = person42;
}
})();
#Angular zFloat Directive
Tells Angular to display the view value rather than the floating point
model value when the view and model values are "equivalent".
## What's the problem
Without this directive, Angular data binding can erase the user's input as she
is typing, making it difficult for her to complete her intended data value.
The cause is Angular's eager data binding.
Angular sends *each keystroke* through to the data bound model property.
That becomes a problem when Breeze parses data entry before
the user has finished typing. The user could be in the middle of data entry
when Breeze parsing does something to her intermediate value and
updates the property with something else *before she has a chance to
complete her thought*.
This *something else* is (or should be) "equivalent" to the current value
displayed in the control. But it may not be identical, "letter-for-letter".
This is a problem for floating point properties (decimal, single, double, float).
For example (shown here), suppose the user tries to enter the decimal value, 250.05.
She types "2", "5", "0". So far so good. Inside the bound model property Breeze
parses these strings to the integers 2, 25, 250.
Then she enters the decimal point ('.'), intending to finish the amount with the $0.05 cents.
The "viewValue" is "250." (the string with the trailing decimal point).
Breeze parses it to its numerical equivalent, the integer 250. The parsed value
is the same as a moment ago so Breeze makes no change to the model property.
But the viewValue ("250.") and the parsed value (250) are no longer identical.
Without "zFloat", the Angular digest cycle re-reads the model property,
sees integer 250, and revises the input box viewValue dropping the decimal point.
**The user never has a chance to enter the digits *after* the decimal point.**
>The same thing happens when the user enters '0's after the decimal.
The user wants to enter "250.05" but can't type the "zero" after the decimal
because Breeze keeps parsing the value to the integer 250 and Angular keeps
re-updating the input box, wiping out the user's last "zero" keystroke.
You can experience these unwanted behaviors in the "**Without zFloat**" textbox.
Now try the same thing in the "**With zFloat**" textbox.
You can enter the full value, taking as long
as you like to complete the floating point number.
## How it works
The directive adds a "floating point equivalence" $formatter to the ngModelController.
This formatter compares the "viewValue" and "modelValue" and returns
the "viewValue" if that string representation is deemed "equivalent"
to the numeric representation in the model.
Because "250." is equivalent to the parsed integer 250, Angular displays
the "250." viewValue ... and the user can continue typing "0", "5" yielding
the intended value of 250.05.
>This directive only applies numeric equivalence.
A future more generic version could apply different equivalence functions
based on the data bound property's data type and might be configurable
with custom equivalence fns.
## Install
This directive is part of the Breeze Labs. Install in three steps:
1. [download](https://github.com/IdeaBlade/Breeze/blob/master/Breeze.Client/Scripts/Labs/breeze.directives.js) the script file (or [install with NuGet](https://www.nuget.org/packages/Breeze.Angular.Directives/))
1. load the breeze.directives module script *after* angular itself.
1. add the 'breeze.directives' module to your app module's dependencies as seen in this example
angular.module('app', ['breeze.directives']);
Now you're ready to apply the directive.
## Usage
Add the `zFloat` attribute directive to the input box HTML.
<input data-ng-model="vm.person.balance" data-z-float>
## Thanks
The significance of the problem was first pointed out to me by
[@qorsmond in a StackOverflow question](http://stackoverflow.com/questions/21997537/breezejs-double-trouble-in-angularjs/22296446).
Thanks @qorsmond for your inspiration.