Skip to content
This repository

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

Open
wants to merge 14 commits into from

3 participants

albertosantini Eric Ferraiuolo Juan Ignacio Dopazo
albertosantini

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

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.

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
albertosantini

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
albertosantini

@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))
  174
+        },
  175
+
  176
+        kelvinChange: function (e) {
  177
+            this.temperatureUpdate(e, this.kelvinNode);
  178
+        }
  179
+    });
  180
+
  181
+    myTemperatureModel = new TemperatureModel();
  182
+    myTemperatureView = new TemperatureView({
  183
+        container: Y.one("#temperatureView"),
  184
+        model: myTemperatureModel
  185
+    });
  186
+
  187
+    // Events are attached lazily.
  188
+    // They will be attached on the first call to getting the container node.
  189
+    myTemperatureView.get('container');
1
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))
  38
+        convertKelvinToFahrenheit: function (value) {
  39
+            this.set('fahrenheit', this.round((value * 9 / 5) - 459.67));
  40
+        },
  41
+
  42
+        convertKelvinToCelsius: function (value) {
  43
+            this.set('celsius', this.round(value - 273.15));
  44
+        }
  45
+
  46
+    }, {
  47
+        // The attributes of the model with the default value and
  48
+        // the validation method, called when the attribute is modified.
  49
+        ATTRS: {
  50
+            fahrenheit: {
  51
+                value: 0,
  52
+                validator: function (value, what) {
  53
+                    return this.check(value, what);
1
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
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
albertosantini

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))
  73
+                validator: Y.Lang.isNumber
  74
+            },
  75
+            celsius: {
  76
+                value: NaN,
  77
+                validator: Y.Lang.isNumber
  78
+            },
  79
+            kelvin: {
  80
+                value: NaN,
  81
+                validator: Y.Lang.isNumber
  82
+            },
  83
+            // This attribute would be a private variable if the model would be
  84
+            // contained in a module, used as value in the `src` attribute of
  85
+            // the `set` method.
  86
+            _CONVERSION: {
  87
+                value: 'conversion'
  88
+            }
1
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))
  127
+
  128
+            // Update the display when a temperature in the model is changed.
  129
+            temperature.after("change", this.render, this);
  130
+
  131
+            // Clear the input fields.
  132
+            this.clear();
  133
+
  134
+            // Events are attached lazily. They will be attached on the first
  135
+            // call to getting the container node.
  136
+            this.get('container');
  137
+        },
  138
+
  139
+        // We round the value to display.
  140
+        round: function (value) {
  141
+            return Math.round(value * 1000) / 1000;
  142
+        },
1
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
albertosantini

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!

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

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 @@
  1
+YUI().use('model', 'view', 'event-valuechange', function (Y) {
  2
+    // Usually the TemperatureModel and TemperatureView classes should be
  3
+    // implemented in separate files as modules. For the sake of the simplicity
  4
+    // those classes are here as local variables and not in a namespace as
  5
+    // Y.TemperatureModel and Y.TemperatureView.
1
Eric Ferraiuolo Owner
ericf added a note September 24, 2012

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))
  8
+        myTemperatureModel,
  9
+        myTemperatureView;
  10
+
  11
+// -- Model --------------------------------------------------------------------
  12
+
  13
+    // The TemperatureModel class extends Y.Model and customizes it to catch the
  14
+    // changes of the properties, setting the value of the conversion for the
  15
+    // rest of the temperature properties.
  16
+
  17
+    TemperatureModel = Y.Base.create('temperatureModel', Y.Model, [], {
  18
+        // The initializer runs when a TemperatureModel instance is created and
  19
+        // gives us an opportunity to set up the events for the attributes.
  20
+        initializer: function () {
  21
+            this.after('fahrenheitChange', this._afterFahrenheitChange);
  22
+            this.after('celsiusChange', this._afterCelsiusChange);
  23
+            this.after('kelvinChange', this._afterKelvinChange);
1
Eric Ferraiuolo Owner
ericf added a note September 24, 2012

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
albertosantini

@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))
  23
+        _afterUnitChange: function (e) {
  24
+            var value = e.newVal,
  25
+                attrName = e.attrName,
  26
+                // The variables containing the math of the conversion.
  27
+                fahrenheitToKelvin,
  28
+                fahrenheitToCelsius,
  29
+                celsiusToFahrenheit,
  30
+                celsiusToKelvin,
  31
+                kelvinToFahrenheit,
  32
+                kelvinToCelsius,
  33
+                // The clientID variable contains a costant value used in
  34
+                // `src` property of the set method. The built-in attribute
  35
+                // clientId is an unique ID automatically generated for
  36
+                // identifying model instances. Any constant value would be
  37
+                // fine.
  38
+                clientId = this.get('clientId');
1
Eric Ferraiuolo Owner
ericf added a note September 24, 2012

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))
  27
+                fahrenheitToKelvin,
  28
+                fahrenheitToCelsius,
  29
+                celsiusToFahrenheit,
  30
+                celsiusToKelvin,
  31
+                kelvinToFahrenheit,
  32
+                kelvinToCelsius,
  33
+                // The clientID variable contains a costant value used in
  34
+                // `src` property of the set method. The built-in attribute
  35
+                // clientId is an unique ID automatically generated for
  36
+                // identifying model instances. Any constant value would be
  37
+                // fine.
  38
+                clientId = this.get('clientId');
  39
+
  40
+            // The `src` property identifies who initiated the `set()` action
  41
+            // and prevent infinite loops or other similar situations.
  42
+            if (e.src !== clientId && attrName === 'fahrenheit') {
1
Eric Ferraiuolo Owner
ericf added a note September 24, 2012

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))
  29
+                celsiusToFahrenheit,
  30
+                celsiusToKelvin,
  31
+                kelvinToFahrenheit,
  32
+                kelvinToCelsius,
  33
+                // The clientID variable contains a costant value used in
  34
+                // `src` property of the set method. The built-in attribute
  35
+                // clientId is an unique ID automatically generated for
  36
+                // identifying model instances. Any constant value would be
  37
+                // fine.
  38
+                clientId = this.get('clientId');
  39
+
  40
+            // The `src` property identifies who initiated the `set()` action
  41
+            // and prevent infinite loops or other similar situations.
  42
+            if (e.src !== clientId && attrName === 'fahrenheit') {
  43
+                fahrenheitToKelvin = (value + 459.67) * 5 / 9;
  44
+                this.set('kelvin', fahrenheitToKelvin, {src: clientId});
1
Eric Ferraiuolo Owner
ericf added a note September 24, 2012

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 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
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
albertosantini parseFloat has not the radix as second argument. 9e97c01
src/app/docs/app/partials/app-temperature-js.mustache
((47 lines not shown))
  47
+                fahrenheit = (kelvin * 9 / 5) - 459.67;
  48
+                celsius = kelvin - 273.15;
  49
+            }
  50
+
  51
+            this.setAttrs({
  52
+                fahrenheit: fahrenheit,
  53
+                celsius: celsius,
  54
+                kelvin: kelvin
  55
+            }, {
  56
+                src: 'conversion'
  57
+            });
  58
+        },
  59
+
  60
+        // This is a method used in the initialization of the attributes.
  61
+        _unitSetter: function (val) {
  62
+            return Y.Lang.isNumber(val) ? val : NaN;
1
Eric Ferraiuolo Owner
ericf added a note September 27, 2012

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))
  60
+        // This is a method used in the initialization of the attributes.
  61
+        _unitSetter: function (val) {
  62
+            return Y.Lang.isNumber(val) ? val : NaN;
  63
+        }
  64
+
  65
+    }, {
  66
+        // The attributes of the model with the default value and the setter
  67
+        // method, called when the attribute is modified. The default value is
  68
+        // not a number because the first conversion would not work if the user
  69
+        // input would be that number: the new value would be equal to the old
  70
+        // value, the default one. It is used a setter (vs. validation) method
  71
+        // because the default value would not pass the validation process, not
  72
+        // allowing the reset of the model.
  73
+        ATTRS: {
  74
+            fahrenheit: {
  75
+                value: NaN,
2
Eric Ferraiuolo Owner
ericf added a note September 27, 2012

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))
  65
+    }, {
  66
+        // The attributes of the model with the default value and the setter
  67
+        // method, called when the attribute is modified. The default value is
  68
+        // not a number because the first conversion would not work if the user
  69
+        // input would be that number: the new value would be equal to the old
  70
+        // value, the default one. It is used a setter (vs. validation) method
  71
+        // because the default value would not pass the validation process, not
  72
+        // allowing the reset of the model.
  73
+        ATTRS: {
  74
+            fahrenheit: {
  75
+                value: NaN,
  76
+                setter: this._unitSetter
  77
+            },
  78
+            celsius: {
  79
+                value: NaN,
  80
+                setter: this._unitSetter
1
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.

albertosantini

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. :)

albertosantini

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

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

Showing 14 unique commits by 1 author.

Sep 21, 2012
albertosantini Add temperature example to App framework. 6dcca80
Sep 22, 2012
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
Sep 23, 2012
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
albertosantini Remove _CONVERSION attr and use clientId prop. 2f59530
albertosantini Add temperature example tests.
At the moment they fail, because I do not know how to simulate
valuechange.
8b6f01e
Sep 24, 2012
albertosantini Put the model and the view classes in Y namespace. e798619
albertosantini Use only one event handler for unit change. c9189b5
albertosantini Use conversion string in src. d040426
albertosantini Refactor unit conversion. e1f461c
albertosantini Fix a typo. 6db4151
Sep 25, 2012
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
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
albertosantini parseFloat has not the radix as second argument. 9e97c01
Sep 28, 2012
albertosantini Remove _unitSetter and the validator of the attrs. ac85cc6
This page is out of date. Refresh to see the latest.
35  src/app/docs/app/app-temperature.mustache
... ...
@@ -0,0 +1,35 @@
  1
+<style scoped>
  2
+    label {
  3
+        width: 80px;
  4
+        float: left;
  5
+        font-weight: bold;
  6
+    }
  7
+</style>
  8
+
  9
+<div class="intro">
  10
+    <p>
  11
+        This example demonstrates how to create a simple temperature converter
  12
+        using the <a href="../model/index.html">Model</a>
  13
+        and <a href="../view/index.html">View</a> components, highlighting
  14
+        validation and change events features.
  15
+    </p>
  16
+</div>
  17
+
  18
+<div class="example">
  19
+    {{>app-temperature-html}}
  20
+    <script>
  21
+        {{>app-temperature-js}}
  22
+    </script>
  23
+</div>
  24
+
  25
+<h2>Complete Example Source</h2>
  26
+
  27
+<h3>JavaScript</h3>
  28
+```
  29
+{{>app-temperature-js}}
  30
+```
  31
+
  32
+<h3>HTML</h3>
  33
+```
  34
+{{>app-temperature-html}}
  35
+```
66  src/app/docs/app/assets/app-temperature-tests.js
... ...
@@ -0,0 +1,66 @@
  1
+YUI.add('app-temperature-tests', function (Y) {
  2
+
  3
+var Assert = Y.Assert,
  4
+
  5
+    fahrenheitSelector = '#fahrenheit',
  6
+    celsiusSelector = '#celsius',
  7
+    kelvinSelector = '#kelvin',
  8
+    fahrenheitNode = Y.one(fahrenheitSelector),
  9
+    celsiusNode = Y.one(celsiusSelector),
  10
+    kelvinNode = Y.one(kelvinSelector),
  11
+
  12
+    suite = new Y.Test.Suite('Temperature Example Suite');
  13
+
  14
+suite.add(new Y.Test.Case({
  15
+    name: 'Example Tests',
  16
+
  17
+    'Converting Fahrenheit temperature': function () {
  18
+        Assert.isNotNull(fahrenheitNode, 'Fahrenheit input field not found.');
  19
+
  20
+        fahrenheitNode.focus();
  21
+        fahrenheitNode.set('value', '32');
  22
+
  23
+        this.wait(function () {
  24
+            Assert.areSame(0, parseFloat(celsiusNode.get('value')),
  25
+                'Conversion from Fahrenheit to Celsius');
  26
+            Assert.areSame(273.15, parseFloat(kelvinNode.get('value')),
  27
+                'Conversion from Fahrenheit to Kelvin');
  28
+        }, 500);
  29
+    },
  30
+
  31
+    'Converting Celsius temperature': function () {
  32
+        Assert.isNotNull(celsiusNode, 'Celsius input field not found.');
  33
+
  34
+        celsiusNode.focus();
  35
+        celsiusNode.set('value', '30');
  36
+
  37
+        this.wait(function () {
  38
+            Assert.areSame(86, parseFloat(fahrenheitNode.get('value')),
  39
+                'Conversion from Celsius to Fahrenheit');
  40
+            Assert.areSame(303.15, parseFloat(kelvinNode.get('value')),
  41
+                'Conversion from Celsius to Kelvin');
  42
+        }, 500);
  43
+    },
  44
+
  45
+    'Converting Kelvin temperature': function () {
  46
+        Assert.isNotNull(kelvinNode, 'Kelvin input field not found.');
  47
+
  48
+        kelvinNode.focus();
  49
+        kelvinNode.set('value', '273.15');
  50
+
  51
+        this.wait(function () {
  52
+            Assert.areSame(32, parseFloat(fahrenheitNode.get('value')),
  53
+                'Conversion from Kelvin to Fahrenheit');
  54
+            Assert.areSame(0, parseFloat(celsiusNode.get('value')),
  55
+                'Conversion from Kelvin to Celsius');
  56
+        }, 500);
  57
+    }
  58
+
  59
+}));
  60
+
  61
+Y.Test.Runner.add(suite);
  62
+
  63
+}, '@VERSION@', {
  64
+    requires: ['node', 'node-event-simulate']
  65
+});
  66
+
10  src/app/docs/app/component.json
@@ -33,6 +33,16 @@
33 33
                 "app", "browsing", "model", "model-list", "naivgation", "pjax",
34 34
                 "view", "handlebars", "template"
35 35
             ]
  36
+        },
  37
+
  38
+        {
  39
+            "name"       : "app-temperature",
  40
+            "displayName": "Temperature Converter",
  41
+            "description": "A basic temperature converter between Celsius, Fahrenheit and Kelvin.",
  42
+            "modules"    : ["model", "view", "event-valuechange"],
  43
+            "tags"       : [
  44
+                "model", "view", "event-valuechange"
  45
+            ]
36 46
         }
37 47
     ]
38 48
 }
19  src/app/docs/app/partials/app-temperature-html.mustache
... ...
@@ -0,0 +1,19 @@
  1
+<div id="demo">
  2
+    <p>Pick a field and type a number to get the conversion of the temperature.
  3
+    </p>
  4
+
  5
+    <div id="temperatureView">
  6
+        <p>
  7
+            <label for="fahrenheit">Fahrenheit:</label>
  8
+            <input type ="text" id="fahrenheit" />
  9
+        </p>
  10
+        <p>
  11
+            <label for="celsius">Celsius:</label>
  12
+            <input type="text" id="celsius" />
  13
+        </p>
  14
+        <p>
  15
+            <label for="kelvin">Kelvin:</label>
  16
+            <input type="text" id="kelvin" />
  17
+        </p>
  18
+    </div>
  19
+</div>
182  src/app/docs/app/partials/app-temperature-js.mustache
... ...
@@ -0,0 +1,182 @@
  1
+YUI().use('model', 'view', 'event-valuechange', function (Y) {
  2
+    var myTemperatureModel,
  3
+        myTemperatureView;
  4
+
  5
+// -- Model --------------------------------------------------------------------
  6
+
  7
+    // The TemperatureModel class extends Y.Model and customizes it to catch the
  8
+    // changes of the properties, setting the value of the conversion for the
  9
+    // rest of the temperature properties.
  10
+
  11
+    Y.TemperatureModel = Y.Base.create('temperatureModel', Y.Model, [], {
  12
+        // The initializer runs when a TemperatureModel instance is created and
  13
+        // gives us an opportunity to set up the events for the attributes.
  14
+        initializer: function () {
  15
+            this.after([
  16
+                'fahrenheitChange',
  17
+                'celsiusChange',
  18
+                'kelvinChange'
  19
+            ], this._afterUnitChange);
  20
+        },
  21
+
  22
+        // Event handler for the 'change' event.
  23
+        _afterUnitChange: function (e) {
  24
+            var newVal = e.newVal,
  25
+                attrName = e.attrName,
  26
+                // The variables containing the math of the conversion.
  27
+                fahrenheit,
  28
+                celsius,
  29
+                kelvin;
  30
+
  31
+            // The `src` property identifies who initiated the `set()` action
  32
+            // and prevent infinite loops or other similar situations.
  33
+            if (e.src === 'conversion') {
  34
+                return;
  35
+            }
  36
+
  37
+            if (attrName === 'fahrenheit') {
  38
+                fahrenheit = newVal;
  39
+                celsius = (fahrenheit - 32) * 5 / 9;
  40
+                kelvin = (fahrenheit + 459.67) * 5 / 9;
  41
+            } else if (attrName === 'celsius') {
  42
+                celsius = newVal;
  43
+                fahrenheit = (celsius * 9 / 5) + 32;
  44
+                kelvin = celsius + 273.15;
  45
+            } else if (attrName === 'kelvin') {
  46
+                kelvin = newVal;
  47
+                fahrenheit = (kelvin * 9 / 5) - 459.67;
  48
+                celsius = kelvin - 273.15;
  49
+            }
  50
+
  51
+            this.setAttrs({
  52
+                fahrenheit: fahrenheit,
  53
+                celsius: celsius,
  54
+                kelvin: kelvin
  55
+            }, {
  56
+                src: 'conversion'
  57
+            });
  58
+        }
  59
+
  60
+    }, {
  61
+        // The attributes of the model with the default value and the setter
  62
+        // method, called when the attribute is modified. The default value is
  63
+        // not a number because the first conversion would not work if the user
  64
+        // input would be that number: the new value would be equal to the old
  65
+        // value, the default one.
  66
+        ATTRS: {
  67
+            fahrenheit: {value: NaN},
  68
+            celsius: {value: NaN},
  69
+            kelvin: {value: NaN}
  70
+        }
  71
+    });
  72
+
  73
+// -- View ---------------------------------------------------------------------
  74
+
  75
+    // The TemperatureView class extends Y.View and customizes it to represent
  76
+    // the form containing the input fields of the temperatures. It also handles
  77
+    // DOM events to allow the conversion.
  78
+
  79
+    Y.TemperatureView = Y.Base.create('temperatureView', Y.View, [], {
  80
+        // These are custom attribute that we will use to hold a reference to
  81
+        // the DOM input field.
  82
+        fahrenheitNode: Y.one('#fahrenheit'),
  83
+        celsiusNode: Y.one('#celsius'),
  84
+        kelvinNode: Y.one('#kelvin'),
  85
+
  86
+        // This is where we attach DOM events for the view. The `events` object
  87
+        // is a mapping of selectors to an object containing one or more events
  88
+        // to attach to the node(s) matching each selector.
  89
+        events: {
  90
+            '#fahrenheit': { valuechange: 'fahrenheitChange' },
  91
+            '#celsius': { valuechange: 'celsiusChange' },
  92
+            '#kelvin': { valuechange: 'kelvinChange' }
  93
+        },
  94
+
  95
+        // The initializer runs when a TemperatureView instance is created and
  96
+        // gives us an opportunity to set up the view.
  97
+        initializer: function () {
  98
+            // The model property is set to a TemperatureModel instance when
  99
+            // this view, TemperatureView, is created.
  100
+            var temperature = this.get('model');
  101
+
  102
+            // Update the display when a temperature in the model is changed.
  103
+            temperature.after("change", this.render, this);
  104
+
  105
+            // Events are attached lazily. They will be attached on the first
  106
+            // call to getting the container node.
  107
+            this.get('container');
  108
+        },
  109
+
  110
+        // We format the value to display.
  111
+        format: function (value) {
  112
+            // If the value to display contained in the model is the default
  113
+            // value, that is NaN, the return value is an empty string, clearing
  114
+            // the input field.
  115
+            return Y.Lang.isNumber(value) ?
  116
+                    Math.round(value * 1000) / 1000 : '';
  117
+        },
  118
+
  119
+        // The render function is called whenever a temperature item is changed,
  120
+        // thanks to the list event handler we attached in the initializer
  121
+        // above.
  122
+        render: function () {
  123
+            // The model provides the temperature values.
  124
+            var temperatureModel = this.get('model'),
  125
+                fahrenheit = temperatureModel.get('fahrenheit'),
  126
+                celsius = temperatureModel.get('celsius'),
  127
+                kelvin = temperatureModel.get('kelvin');
  128
+
  129
+            // The temperatures are displayed.
  130
+            this.fahrenheitNode.set('value', this.format(fahrenheit));
  131
+            this.celsiusNode.set('value', this.format(celsius));
  132
+            this.kelvinNode.set('value', this.format(kelvin));
  133
+        },
  134
+
  135
+        // The common method of the event handlers.
  136
+        temperatureUpdate: function (e, node) {
  137
+            var temperatureType,
  138
+                temperatureValue,
  139
+                temperatureModel = this.get('model');
  140
+
  141
+            // We trim the value to remove the spaces and force the input field
  142
+            // to contain only numbers. A more robust approach is suggested, but
  143
+            // it is out of scope of this example.
  144
+            temperatureValue = parseFloat(Y.Lang.trim(node.get('value')));
  145
+
  146
+            if (!Y.Lang.isNumber(temperatureValue)) {
  147
+                // If an input field is empty or is not a number, the model is
  148
+                // reset and the view is rendered because the attributes are
  149
+                // changed.
  150
+                temperatureModel.reset();
  151
+            } else {
  152
+                // The type values are the following: fahrenheit, celsius or
  153
+                // kelvin.
  154
+                temperatureType = node.get('id');
  155
+
  156
+                // Update the temperature in the model.
  157
+                temperatureModel.set(temperatureType, temperatureValue);
  158
+            }
  159
+        },
  160
+
  161
+        // Event handlers for the event 'valuechange'.
  162
+        fahrenheitChange: function (e) {
  163
+            this.temperatureUpdate(e, this.fahrenheitNode);
  164
+        },
  165
+
  166
+        celsiusChange: function (e) {
  167
+            this.temperatureUpdate(e, this.celsiusNode);
  168
+        },
  169
+
  170
+        kelvinChange: function (e) {
  171
+            this.temperatureUpdate(e, this.kelvinNode);
  172
+        }
  173
+    });
  174
+
  175
+// -- Start your engines! ------------------------------------------------------
  176
+
  177
+    myTemperatureModel = new Y.TemperatureModel();
  178
+    myTemperatureView = new Y.TemperatureView({
  179
+        container: Y.one('#temperatureView'),
  180
+        model: myTemperatureModel
  181
+    });
  182
+});
Commit_comment_tip

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.