Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

[wip] Add temperature example to App framework. #276

Closed
wants to merge 14 commits into from

4 participants

Alberto Santini Eric Ferraiuolo Juan Ignacio Dopazo Clarence Leung
Alberto Santini

This example demonstrates how to create a simple temperature converter using Model and View components, highlighting validation and events features.

Eric Ferraiuolo ericf was assigned
Eric Ferraiuolo
Owner

Cool! I think this example will be good for showing the basic interaction between Models and Views in a way that will be really helpful for people.

Eric Ferraiuolo
Owner

Looking through the code I see one major thing that jumps out at me. It appears that the View is doing some of the Model's work. Could you refactor this such that the Model keeps its attribute values in sync when one of them changes? To avoid an infinite loops, you'll want to utilize the src option when calling set().

Also, instead of using the keydown event, I'd recommend switching to valuechange which is provided by the event-valuechange module.

Alberto Santini albertosantini Refactor Model and View for the readability.
Change keydown event with valuechange.
Move convert methods from the view to the model.
Rename convert methods of the view to update.
Add a few comments and rename variables just to be more clear.
Fix a typo in app-temperature.mustache.
Add event-valuechange in modules and tags in component.json.
c64506a
Alberto Santini

Thanks Eric for the insightful advices. I hope I implemented them correctly.

I didn't get src option.

For instance, if I add it to the set

this.set('celsius', this.round((value - 32) * 5 / 9), {src: 'fahrenheit'});

I didn't understand how to use it.

Juan Ignacio Dopazo
Collaborator

@albertosantini the src property is almost a standard in YUI code to identify who initiated the set() action and prevent infinite loops or other similar situations. Eric proposes that every attribute representing a temperature unit has a listener for its change event that modifies the other units. For example:

_onCelsiusChange: function (e) {
  this.set('kelvin', this._celsiusToKelvin(e.newVal));
  this.set('fahrenheit', this._celsiusToFarenheit(e.newVal));
}

However, if the afterKelvinChange event listener changed the celsius attribute, you'd have an infinite loop. So what you do is "mark" the setting action as "this came from a change listener" so that it doesn't change other attributes. For example:

var TRANSFORM = 'transform';
/* ... */
_onCelsiusChange: function (e) {
  if (e.src !== TRANSFORM) {
    this.set('kelvin', this._celsiusToKelvin(e.newVal), {src: TRANSFORM});
    this.set('fahrenheit', this._celsiusToFarenheit(e.newVal), {src: TRANSFORM});
  }
}

This way the View will only have to:

  • Listen to the change event of the Model to re-render
  • When the user changes the value of an input, change the value of the corresponding unit in the model
Alberto Santini

@juandopazo Thanks for the details.

I think I don't understand the last detail: I cannot figure out where I should write
model.on('celsiuschange', _onCelsiusChange);

In the initializer of the view?
Using myTemperatureModel in the 'main' (I mean outside the model and the view instance)?
Or there is a default method I should override in the model (I tried _onCelsiusChange)?

Juan Ignacio Dopazo
Collaborator

In the Model's initializer, listen for the change event of each unit and change the others:

/* ... */
initializer: function () {
  this.after('celsiusChange', this._afterCelsiusChange);
  this.after('fahrenheitChange', this._afterFahrenheitChange);
  this.after('kelvinChange', this._afterKelvinChange);
},
_afterCelsiusChange: function (e) {
  if (e.src !== TRANSFORM) {
    this.set('kelvin', this._celsiusToKelvin(e.newVal), {src: TRANSFORM});
    this.set('fahrenheit', this._celsiusToFarenheit(e.newVal), {src: TRANSFORM});
  }
}
/* ... */

In the View's initializer, listen for the change event and update the DOM:

initializer: function () {
  var temperature = this.get('model');
  temperature.after('change', this.updateView, this);
  this.clear();
}
src/app/docs/app/partials/app-temperature-js.mustache
((174 lines not shown))
+ },
+
+ kelvinChange: function (e) {
+ this.temperatureUpdate(e, this.kelvinNode);
+ }
+ });
+
+ myTemperatureModel = new TemperatureModel();
+ myTemperatureView = new TemperatureView({
+ container: Y.one("#temperatureView"),
+ model: myTemperatureModel
+ });
+
+ // Events are attached lazily.
+ // They will be attached on the first call to getting the container node.
+ myTemperatureView.get('container');
Juan Ignacio Dopazo Collaborator

Shouldn't this be in the render method of the View?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
src/app/docs/app/partials/app-temperature-js.mustache
((38 lines not shown))
+ convertKelvinToFahrenheit: function (value) {
+ this.set('fahrenheit', this.round((value * 9 / 5) - 459.67));
+ },
+
+ convertKelvinToCelsius: function (value) {
+ this.set('celsius', this.round(value - 273.15));
+ }
+
+ }, {
+ // The attributes of the model with the default value and
+ // the validation method, called when the attribute is modified.
+ ATTRS: {
+ fahrenheit: {
+ value: 0,
+ validator: function (value, what) {
+ return this.check(value, what);
Juan Ignacio Dopazo Collaborator

You may want to simplify this. I understand you probably did it for educational reasons, but it's a wrapper for a wrapper for Y.Lang.isNumber.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Alberto Santini albertosantini Better refactoring of TemperatureModel.
Add change events to the model initializer.
Add src property to the sets of the model to avoid recursion.
Move round method to the view.
Move the init of the view events to the view initializer.
Add a few comments.
5b8dbf1
Alberto Santini

I think I implemented all the suggestions. :)

Anything else?

As usual I learnt a lot of things.

Thanks Juan and Eric.

src/app/docs/app/partials/app-temperature-js.mustache
((73 lines not shown))
+ validator: Y.Lang.isNumber
+ },
+ celsius: {
+ value: NaN,
+ validator: Y.Lang.isNumber
+ },
+ kelvin: {
+ value: NaN,
+ validator: Y.Lang.isNumber
+ },
+ // This attribute would be a private variable if the model would be
+ // contained in a module, used as value in the `src` attribute of
+ // the `set` method.
+ _CONVERSION: {
+ value: 'conversion'
+ }
Juan Ignacio Dopazo Collaborator

We usually use a variable to avoid typos, but it can be just a string. Using an attribute is probably overkill and maybe confusing, you could use a class property TemperatureModel.CONVERSION if you don't want it to be a private variable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
src/app/docs/app/partials/app-temperature-js.mustache
((127 lines not shown))
+
+ // Update the display when a temperature in the model is changed.
+ temperature.after("change", this.render, this);
+
+ // Clear the input fields.
+ this.clear();
+
+ // Events are attached lazily. They will be attached on the first
+ // call to getting the container node.
+ this.get('container');
+ },
+
+ // We round the value to display.
+ round: function (value) {
+ return Math.round(value * 1000) / 1000;
+ },
Juan Ignacio Dopazo Collaborator

This is presentation, you're right. Spot on.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Alberto Santini

I removed the fake attribute _CONVERSION used with the src.
I used the built-in property clientId.

It seems to me elegant, because there is not need to add any variable.

Gracias Juan.

Juan Ignacio Dopazo
Collaborator

Don't thank me. I never get around to writing examples. It's easier to criticize. :tongue:

Kudos on the initiative!

Alberto Santini albertosantini Add temperature example tests.
At the moment they fail, because I do not know how to simulate
valuechange.
8b6f01e
Alberto Santini

I added the tests for the example, but they fail, because I do not know how to simulate valuechange.

Then I found a bug in the example: if I type, for instance, '3' in the fahrenheit input field, the conversion is displayed; then I type backspace and the fields are cleared; if I retype '3', the conversion is not done because there is not a change in the model (fahrenheit attribute contains 3).

So I added temperatureModel.reset() after clearing the fields in temperatureUpdate(): now the attributes should contain the initial values (NaN), but the fahrenheit attribute contains the value 3.

What am I missing for the valuechange simulation and the reset of the model?

src/app/docs/app/partials/app-temperature-js.mustache
@@ -0,0 +1,206 @@
+YUI().use('model', 'view', 'event-valuechange', function (Y) {
+ // Usually the TemperatureModel and TemperatureView classes should be
+ // implemented in separate files as modules. For the sake of the simplicity
+ // those classes are here as local variables and not in a namespace as
+ // Y.TemperatureModel and Y.TemperatureView.
Eric Ferraiuolo Owner
ericf added a note

I actually think it's easier for people to distinguish between classes and variables, when the classes hang off the Y object.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
src/app/docs/app/partials/app-temperature-js.mustache
((8 lines not shown))
+ myTemperatureModel,
+ myTemperatureView;
+
+// -- Model --------------------------------------------------------------------
+
+ // The TemperatureModel class extends Y.Model and customizes it to catch the
+ // changes of the properties, setting the value of the conversion for the
+ // rest of the temperature properties.
+
+ TemperatureModel = Y.Base.create('temperatureModel', Y.Model, [], {
+ // The initializer runs when a TemperatureModel instance is created and
+ // gives us an opportunity to set up the events for the attributes.
+ initializer: function () {
+ this.after('fahrenheitChange', this._afterFahrenheitChange);
+ this.after('celsiusChange', this._afterCelsiusChange);
+ this.after('kelvinChange', this._afterKelvinChange);
Eric Ferraiuolo Owner
ericf added a note

I'd like to see one attribute change event handler used for all of the temperature attributes. The event handler can inspect the attribute which has changed, and apply the conversions appropriately. Then the event subscription can look like this:

this.after([
    'fahrenheitChange',
    'celsiusChange',
    'kelvinChange'
], this._afterUnitChange);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Alberto Santini

@ericf Done.

Any hint for valuechange in the tests and the model reset?

I think the problem with the reset is the default value NaN.

src/app/docs/app/partials/app-temperature-js.mustache
((23 lines not shown))
+ _afterUnitChange: function (e) {
+ var value = e.newVal,
+ attrName = e.attrName,
+ // The variables containing the math of the conversion.
+ fahrenheitToKelvin,
+ fahrenheitToCelsius,
+ celsiusToFahrenheit,
+ celsiusToKelvin,
+ kelvinToFahrenheit,
+ kelvinToCelsius,
+ // The clientID variable contains a costant value used in
+ // `src` property of the set method. The built-in attribute
+ // clientId is an unique ID automatically generated for
+ // identifying model instances. Any constant value would be
+ // fine.
+ clientId = this.get('clientId');
Eric Ferraiuolo Owner
ericf added a note

I liked the old idea of using the string "conversion" as the event source.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
src/app/docs/app/partials/app-temperature-js.mustache
((27 lines not shown))
+ fahrenheitToKelvin,
+ fahrenheitToCelsius,
+ celsiusToFahrenheit,
+ celsiusToKelvin,
+ kelvinToFahrenheit,
+ kelvinToCelsius,
+ // The clientID variable contains a costant value used in
+ // `src` property of the set method. The built-in attribute
+ // clientId is an unique ID automatically generated for
+ // identifying model instances. Any constant value would be
+ // fine.
+ clientId = this.get('clientId');
+
+ // The `src` property identifies who initiated the `set()` action
+ // and prevent infinite loops or other similar situations.
+ if (e.src !== clientId && attrName === 'fahrenheit') {
Eric Ferraiuolo Owner
ericf added a note

The common pattern in these if statements is checking if the source is the clientId (which I think the string "conversion" is better, as noted above), and handling the event as noop.

You could mov this to the top of this function:

if (e.src === 'conversion') { return; }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
src/app/docs/app/partials/app-temperature-js.mustache
((29 lines not shown))
+ celsiusToFahrenheit,
+ celsiusToKelvin,
+ kelvinToFahrenheit,
+ kelvinToCelsius,
+ // The clientID variable contains a costant value used in
+ // `src` property of the set method. The built-in attribute
+ // clientId is an unique ID automatically generated for
+ // identifying model instances. Any constant value would be
+ // fine.
+ clientId = this.get('clientId');
+
+ // The `src` property identifies who initiated the `set()` action
+ // and prevent infinite loops or other similar situations.
+ if (e.src !== clientId && attrName === 'fahrenheit') {
+ fahrenheitToKelvin = (value + 459.67) * 5 / 9;
+ this.set('kelvin', fahrenheitToKelvin, {src: clientId});
Eric Ferraiuolo Owner
ericf added a note

I was thinking more along the lines of having the conditional branch based on e.attrName, and perform the conversion in each block of the conditional. They way we'd have all the new values setup and we can do one big setAttrs() call at the end of this function.

_afterUnitChange: function (e) {
    if (e.src === 'conversion') { return; }

    var newVal   = e.newVal,
        attrName = e.attrName,
        celsius, fahrenheit, kelvin;

    if (attrName === 'celsius') {
        celsius    = newVal;
        fahrenheit = (celsius * 9 / 5) + 32;
        kelvin     = celsius + 273.15;
    } else if (attrName === 'fahrenheit') {
        fahrenheit = newVal;
        celsius    = (fahrenheit - 32) * 5 / 9;
        kelvin     = (fahrenheit + 459.67) * 5 / 9;
    } else if (attrName === 'kelvin') {
        kelvin     = newVal;
        celsius    = kelvin - 273.15;
        fahrenheit = (kelvin * 9 / 5) - 459.67;
    }

    this.setAttrs({
        celsius   : celsius,
        fahrenheit: fahrenheit,
        kelvin    : kelvin
    }, {src: 'conversion'});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Eric Ferraiuolo
Owner
albertosantini added some commits
Alberto Santini albertosantini Fix the problem with reset using a setter method.
The model is not reset if the attribute doesn't pass the validation
process. If the default value is out of scope of the validation, it is
impossible to reset the model.
3e8e974
Alberto Santini albertosantini All tests passed.
Adding the focus to the node and waiting a few milliseconds, just to give
time to the polling of the valuechange to catch the change, the tests are
fine.
5aa0acf
Alberto Santini albertosantini parseFloat has not the radix as second argument. 9e97c01
src/app/docs/app/partials/app-temperature-js.mustache
((47 lines not shown))
+ fahrenheit = (kelvin * 9 / 5) - 459.67;
+ celsius = kelvin - 273.15;
+ }
+
+ this.setAttrs({
+ fahrenheit: fahrenheit,
+ celsius: celsius,
+ kelvin: kelvin
+ }, {
+ src: 'conversion'
+ });
+ },
+
+ // This is a method used in the initialization of the attributes.
+ _unitSetter: function (val) {
+ return Y.Lang.isNumber(val) ? val : NaN;
Eric Ferraiuolo Owner
ericf added a note

I think this is a bit confusing… I'm curious what you're trying to do here. Also, technically NaN is a number :-/

console.log(typeof NaN); // => "number"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
src/app/docs/app/partials/app-temperature-js.mustache
((60 lines not shown))
+ // This is a method used in the initialization of the attributes.
+ _unitSetter: function (val) {
+ return Y.Lang.isNumber(val) ? val : NaN;
+ }
+
+ }, {
+ // The attributes of the model with the default value and the setter
+ // method, called when the attribute is modified. The default value is
+ // not a number because the first conversion would not work if the user
+ // input would be that number: the new value would be equal to the old
+ // value, the default one. It is used a setter (vs. validation) method
+ // because the default value would not pass the validation process, not
+ // allowing the reset of the model.
+ ATTRS: {
+ fahrenheit: {
+ value: NaN,
Eric Ferraiuolo Owner
ericf added a note

I think that defaulting these to null or simply leaving them undefined would be better. What did you run into where you wanted to use NaN?

Short story: if there is a validator, for instance Y.Lang.isNumber, and the default value is NaN, the model is not reset. I mean, when the reset method is called, the validator is called and if an attribute does not pass the validation, the model is not reset, that is the attributes do not contain the default values.

I do not know if it is the expected behaviour for a reset, I mean for a reset, maybe the validation step should be ignored.
My workaround was the setter use.

I will try to remove the validator or to change it: maybe Y.Lang.isNumber is too strict, accordingly the default value.

tl;dr;

However I recap my attempts, because the path, I think, may be interesting for other users.

Attempt 1: At the beginning the validator was Y.Lang.isNumber and the default value was zero. I didn't reset the model and cleaned (in the view with the method clear) the inputs with an empty string when the value was not a number.
Problem: If the user types the same value of the default one, the conversion does not work because there is not 'change'.

Attempt 2: So I set the default value to NaN and the validator was always Y.Lang.isNumber. When the user cleans the input, the inputs are cleared with an empty string, but the model was not in sync with view. So the attributes contained the last value and not the default one.
Problem: If the user types the same last digit in the last input field where she typed in, the conversion does not work because there is not 'change'. For that attribute the model saved the last value, that's the last digit.

So I recognized it was a bad practice to clear manually the view. If I reset the model, there is not need to use a clear method in the view: change event and the rendering of the view do the work.

Attempt 3: We have always NaN as default value and Y.Lang.isNumber as validator. I removed the clear method in the view and I added the reset of the model when the view is updated.
Problem: When the user clean an input field, the model is not reset, because the default value is NaN and this value is not validated by the validator method. The input fields are not cleared.

I tried to fix it with the setter approach. See the short story above (and this is an infinite loop). :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
src/app/docs/app/partials/app-temperature-js.mustache
((65 lines not shown))
+ }, {
+ // The attributes of the model with the default value and the setter
+ // method, called when the attribute is modified. The default value is
+ // not a number because the first conversion would not work if the user
+ // input would be that number: the new value would be equal to the old
+ // value, the default one. It is used a setter (vs. validation) method
+ // because the default value would not pass the validation process, not
+ // allowing the reset of the model.
+ ATTRS: {
+ fahrenheit: {
+ value: NaN,
+ setter: this._unitSetter
+ },
+ celsius: {
+ value: NaN,
+ setter: this._unitSetter
Juan Ignacio Dopazo Collaborator

this._unitSetter doesn't work.. this will point to the global object. Instead you can use the string '_unitSetter'.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Eric Ferraiuolo
Owner

@albertosantini I wanted to give you an update on this…

Last week I pulled this down locally, ran the example, and I wasn't happy with the UX. This is not your fault, rather I think you've highlighted a very common use case which the YUI App Framework doesn't have a great answer for. I'd like us to really nail a solid, robust solution/pattern that can applied anytime someone gets into a situation like this.

I have a few varieties of changes to this example locally, none of which I think are solid enough yet. But I'll post some ideas here this week and we can continue to discuss and determine a bullet-proof solution for this type of data and user interaction.

Again, I think we really need this example, I just want to make sure we're providing the best possible solution/pattern.

Alberto Santini

I agree. Same feelings here.

I think the long recycle of the pull and a few comments in the outdated diffs reflect what you described.

I am open to any suggestion. :)

Alberto Santini

I read Jeff's post about using after() event listeners for reacting to attribute value changes:

http://fromanegg.com/post/37222972936/using-after-event-listeners-to-react-to-attribute

Do you think is it a feasible approach (vs. event-valuechange)?

Maybe it doesn't apply because the example uses Model and View approach (with Attribute built-in event architecture), but it would be interesting to release the example. :)

Eric Ferraiuolo
Owner

I think we should approach this with the data binding features that are brewing: #386 I think this will also serve as a good test to make sure the data binding features can handle this use case. /cc @lsmith

Clarence Leung
Collaborator

Hi @albertosantini,

I'm closing this due to a bit of a lack of activity, but I'd definitely love to see this continue if you decide working on it.

We can definitely always have it published as a blog post instead, if you want to talk about some of the work you've done here.

Let me know, and cheers!

Clarence Leung clarle closed this
Alberto Santini

Hello @clarle.

Without any news on event side or app framework, the state of art of the example would not be too much different from the proposed.

I agree about closing it.

Cheers!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Sep 21, 2012
  1. Alberto Santini
Commits on Sep 22, 2012
  1. Alberto Santini

    Refactor Model and View for the readability.

    albertosantini authored
    Change keydown event with valuechange.
    Move convert methods from the view to the model.
    Rename convert methods of the view to update.
    Add a few comments and rename variables just to be more clear.
    Fix a typo in app-temperature.mustache.
    Add event-valuechange in modules and tags in component.json.
Commits on Sep 23, 2012
  1. Alberto Santini

    Better refactoring of TemperatureModel.

    albertosantini authored
    Add change events to the model initializer.
    Add src property to the sets of the model to avoid recursion.
    Move round method to the view.
    Move the init of the view events to the view initializer.
    Add a few comments.
  2. Alberto Santini
  3. Alberto Santini

    Add temperature example tests.

    albertosantini authored
    At the moment they fail, because I do not know how to simulate
    valuechange.
Commits on Sep 24, 2012
  1. Alberto Santini
  2. Alberto Santini
  3. Alberto Santini
  4. Alberto Santini
  5. Alberto Santini

    Fix a typo.

    albertosantini authored
Commits on Sep 25, 2012
  1. Alberto Santini

    Fix the problem with reset using a setter method.

    albertosantini authored
    The model is not reset if the attribute doesn't pass the validation
    process. If the default value is out of scope of the validation, it is
    impossible to reset the model.
  2. Alberto Santini

    All tests passed.

    albertosantini authored
    Adding the focus to the node and waiting a few milliseconds, just to give
    time to the polling of the valuechange to catch the change, the tests are
    fine.
  3. Alberto Santini
Commits on Sep 28, 2012
  1. Alberto Santini
This page is out of date. Refresh to see the latest.
35 src/app/docs/app/app-temperature.mustache
View
@@ -0,0 +1,35 @@
+<style scoped>
+ label {
+ width: 80px;
+ float: left;
+ font-weight: bold;
+ }
+</style>
+
+<div class="intro">
+ <p>
+ This example demonstrates how to create a simple temperature converter
+ using the <a href="../model/index.html">Model</a>
+ and <a href="../view/index.html">View</a> components, highlighting
+ validation and change events features.
+ </p>
+</div>
+
+<div class="example">
+ {{>app-temperature-html}}
+ <script>
+ {{>app-temperature-js}}
+ </script>
+</div>
+
+<h2>Complete Example Source</h2>
+
+<h3>JavaScript</h3>
+```
+{{>app-temperature-js}}
+```
+
+<h3>HTML</h3>
+```
+{{>app-temperature-html}}
+```
66 src/app/docs/app/assets/app-temperature-tests.js
View
@@ -0,0 +1,66 @@
+YUI.add('app-temperature-tests', function (Y) {
+
+var Assert = Y.Assert,
+
+ fahrenheitSelector = '#fahrenheit',
+ celsiusSelector = '#celsius',
+ kelvinSelector = '#kelvin',
+ fahrenheitNode = Y.one(fahrenheitSelector),
+ celsiusNode = Y.one(celsiusSelector),
+ kelvinNode = Y.one(kelvinSelector),
+
+ suite = new Y.Test.Suite('Temperature Example Suite');
+
+suite.add(new Y.Test.Case({
+ name: 'Example Tests',
+
+ 'Converting Fahrenheit temperature': function () {
+ Assert.isNotNull(fahrenheitNode, 'Fahrenheit input field not found.');
+
+ fahrenheitNode.focus();
+ fahrenheitNode.set('value', '32');
+
+ this.wait(function () {
+ Assert.areSame(0, parseFloat(celsiusNode.get('value')),
+ 'Conversion from Fahrenheit to Celsius');
+ Assert.areSame(273.15, parseFloat(kelvinNode.get('value')),
+ 'Conversion from Fahrenheit to Kelvin');
+ }, 500);
+ },
+
+ 'Converting Celsius temperature': function () {
+ Assert.isNotNull(celsiusNode, 'Celsius input field not found.');
+
+ celsiusNode.focus();
+ celsiusNode.set('value', '30');
+
+ this.wait(function () {
+ Assert.areSame(86, parseFloat(fahrenheitNode.get('value')),
+ 'Conversion from Celsius to Fahrenheit');
+ Assert.areSame(303.15, parseFloat(kelvinNode.get('value')),
+ 'Conversion from Celsius to Kelvin');
+ }, 500);
+ },
+
+ 'Converting Kelvin temperature': function () {
+ Assert.isNotNull(kelvinNode, 'Kelvin input field not found.');
+
+ kelvinNode.focus();
+ kelvinNode.set('value', '273.15');
+
+ this.wait(function () {
+ Assert.areSame(32, parseFloat(fahrenheitNode.get('value')),
+ 'Conversion from Kelvin to Fahrenheit');
+ Assert.areSame(0, parseFloat(celsiusNode.get('value')),
+ 'Conversion from Kelvin to Celsius');
+ }, 500);
+ }
+
+}));
+
+Y.Test.Runner.add(suite);
+
+}, '@VERSION@', {
+ requires: ['node', 'node-event-simulate']
+});
+
10 src/app/docs/app/component.json
View
@@ -33,6 +33,16 @@
"app", "browsing", "model", "model-list", "naivgation", "pjax",
"view", "handlebars", "template"
]
+ },
+
+ {
+ "name" : "app-temperature",
+ "displayName": "Temperature Converter",
+ "description": "A basic temperature converter between Celsius, Fahrenheit and Kelvin.",
+ "modules" : ["model", "view", "event-valuechange"],
+ "tags" : [
+ "model", "view", "event-valuechange"
+ ]
}
]
}
19 src/app/docs/app/partials/app-temperature-html.mustache
View
@@ -0,0 +1,19 @@
+<div id="demo">
+ <p>Pick a field and type a number to get the conversion of the temperature.
+ </p>
+
+ <div id="temperatureView">
+ <p>
+ <label for="fahrenheit">Fahrenheit:</label>
+ <input type ="text" id="fahrenheit" />
+ </p>
+ <p>
+ <label for="celsius">Celsius:</label>
+ <input type="text" id="celsius" />
+ </p>
+ <p>
+ <label for="kelvin">Kelvin:</label>
+ <input type="text" id="kelvin" />
+ </p>
+ </div>
+</div>
182 src/app/docs/app/partials/app-temperature-js.mustache
View
@@ -0,0 +1,182 @@
+YUI().use('model', 'view', 'event-valuechange', function (Y) {
+ var myTemperatureModel,
+ myTemperatureView;
+
+// -- Model --------------------------------------------------------------------
+
+ // The TemperatureModel class extends Y.Model and customizes it to catch the
+ // changes of the properties, setting the value of the conversion for the
+ // rest of the temperature properties.
+
+ Y.TemperatureModel = Y.Base.create('temperatureModel', Y.Model, [], {
+ // The initializer runs when a TemperatureModel instance is created and
+ // gives us an opportunity to set up the events for the attributes.
+ initializer: function () {
+ this.after([
+ 'fahrenheitChange',
+ 'celsiusChange',
+ 'kelvinChange'
+ ], this._afterUnitChange);
+ },
+
+ // Event handler for the 'change' event.
+ _afterUnitChange: function (e) {
+ var newVal = e.newVal,
+ attrName = e.attrName,
+ // The variables containing the math of the conversion.
+ fahrenheit,
+ celsius,
+ kelvin;
+
+ // The `src` property identifies who initiated the `set()` action
+ // and prevent infinite loops or other similar situations.
+ if (e.src === 'conversion') {
+ return;
+ }
+
+ if (attrName === 'fahrenheit') {
+ fahrenheit = newVal;
+ celsius = (fahrenheit - 32) * 5 / 9;
+ kelvin = (fahrenheit + 459.67) * 5 / 9;
+ } else if (attrName === 'celsius') {
+ celsius = newVal;
+ fahrenheit = (celsius * 9 / 5) + 32;
+ kelvin = celsius + 273.15;
+ } else if (attrName === 'kelvin') {
+ kelvin = newVal;
+ fahrenheit = (kelvin * 9 / 5) - 459.67;
+ celsius = kelvin - 273.15;
+ }
+
+ this.setAttrs({
+ fahrenheit: fahrenheit,
+ celsius: celsius,
+ kelvin: kelvin
+ }, {
+ src: 'conversion'
+ });
+ }
+
+ }, {
+ // The attributes of the model with the default value and the setter
+ // method, called when the attribute is modified. The default value is
+ // not a number because the first conversion would not work if the user
+ // input would be that number: the new value would be equal to the old
+ // value, the default one.
+ ATTRS: {
+ fahrenheit: {value: NaN},
+ celsius: {value: NaN},
+ kelvin: {value: NaN}
+ }
+ });
+
+// -- View ---------------------------------------------------------------------
+
+ // The TemperatureView class extends Y.View and customizes it to represent
+ // the form containing the input fields of the temperatures. It also handles
+ // DOM events to allow the conversion.
+
+ Y.TemperatureView = Y.Base.create('temperatureView', Y.View, [], {
+ // These are custom attribute that we will use to hold a reference to
+ // the DOM input field.
+ fahrenheitNode: Y.one('#fahrenheit'),
+ celsiusNode: Y.one('#celsius'),
+ kelvinNode: Y.one('#kelvin'),
+
+ // This is where we attach DOM events for the view. The `events` object
+ // is a mapping of selectors to an object containing one or more events
+ // to attach to the node(s) matching each selector.
+ events: {
+ '#fahrenheit': { valuechange: 'fahrenheitChange' },
+ '#celsius': { valuechange: 'celsiusChange' },
+ '#kelvin': { valuechange: 'kelvinChange' }
+ },
+
+ // The initializer runs when a TemperatureView instance is created and
+ // gives us an opportunity to set up the view.
+ initializer: function () {
+ // The model property is set to a TemperatureModel instance when
+ // this view, TemperatureView, is created.
+ var temperature = this.get('model');
+
+ // Update the display when a temperature in the model is changed.
+ temperature.after("change", this.render, this);
+
+ // Events are attached lazily. They will be attached on the first
+ // call to getting the container node.
+ this.get('container');
+ },
+
+ // We format the value to display.
+ format: function (value) {
+ // If the value to display contained in the model is the default
+ // value, that is NaN, the return value is an empty string, clearing
+ // the input field.
+ return Y.Lang.isNumber(value) ?
+ Math.round(value * 1000) / 1000 : '';
+ },
+
+ // The render function is called whenever a temperature item is changed,
+ // thanks to the list event handler we attached in the initializer
+ // above.
+ render: function () {
+ // The model provides the temperature values.
+ var temperatureModel = this.get('model'),
+ fahrenheit = temperatureModel.get('fahrenheit'),
+ celsius = temperatureModel.get('celsius'),
+ kelvin = temperatureModel.get('kelvin');
+
+ // The temperatures are displayed.
+ this.fahrenheitNode.set('value', this.format(fahrenheit));
+ this.celsiusNode.set('value', this.format(celsius));
+ this.kelvinNode.set('value', this.format(kelvin));
+ },
+
+ // The common method of the event handlers.
+ temperatureUpdate: function (e, node) {
+ var temperatureType,
+ temperatureValue,
+ temperatureModel = this.get('model');
+
+ // We trim the value to remove the spaces and force the input field
+ // to contain only numbers. A more robust approach is suggested, but
+ // it is out of scope of this example.
+ temperatureValue = parseFloat(Y.Lang.trim(node.get('value')));
+
+ if (!Y.Lang.isNumber(temperatureValue)) {
+ // If an input field is empty or is not a number, the model is
+ // reset and the view is rendered because the attributes are
+ // changed.
+ temperatureModel.reset();
+ } else {
+ // The type values are the following: fahrenheit, celsius or
+ // kelvin.
+ temperatureType = node.get('id');
+
+ // Update the temperature in the model.
+ temperatureModel.set(temperatureType, temperatureValue);
+ }
+ },
+
+ // Event handlers for the event 'valuechange'.
+ fahrenheitChange: function (e) {
+ this.temperatureUpdate(e, this.fahrenheitNode);
+ },
+
+ celsiusChange: function (e) {
+ this.temperatureUpdate(e, this.celsiusNode);
+ },
+
+ kelvinChange: function (e) {
+ this.temperatureUpdate(e, this.kelvinNode);
+ }
+ });
+
+// -- Start your engines! ------------------------------------------------------
+
+ myTemperatureModel = new Y.TemperatureModel();
+ myTemperatureView = new Y.TemperatureView({
+ container: Y.one('#temperatureView'),
+ model: myTemperatureModel
+ });
+});
Something went wrong with that request. Please try again.