Weather Report Guide (Advanced)
This guides you through extending the Simple Weather Report Guide to remove imperative code and automatically look up the user’s location using the browser’s geolocation API. Both of these will be done with event streams.
This guide continues where the Simple Weather Report Guide left off. It takes about 25 minutes to complete. It was written with CanJS 4.1.
The final widget looks like:
Start this tutorial by cloning the following JS Bin:
This is the ending JS Bin for the Simple Weather Report Guide with Kefir.js added.
The following sections are broken down into:
- Problem — A description of what the section is trying to accomplish.
- Things to know — Information about CanJS that is useful for solving the problem.
- Solution — The solution to the problem.
Removing Imperative Code
The problem
Currently, when a new location is set, the place property is set to null:
const WeatherViewModel = can.DefineMap.extend({
location: {
type: "string",
set: function() {
this.place = null;
}
},
...
});
This is imperative code.
It uses side-effects to change the value
of place when location is changed. The rules for how place behaves are not
defined in one place, which makes the code harder to follow.
Instead, we want to completely define the behavior of place within the place definition, which looks like
this:
const WeatherViewModel = can.DefineMap.extend({
...
place: {
type: "any",
get: function(lastSet) {
if (lastSet) {
return lastSet;
} else {
if (this.places && this.places.length === 1) {
return this.places[0];
}
}
}
},
...
});
We want to define the behavior of place so that it becomes null when location changes.
Things to know
DefineMapgetters can only derive a value from other values. They can’t derive a value from the change in other values. However, event-stream libraries like KefirJS can do this.For example, we can create a
Kefirstream that counts the number of times the followingpersonmap’snameproperty changes using the can-stream-kefir module as follows:const person = new can.DefineMap({name: "Justin"}); // Create a stream from person’s name const nameStream = can.streamKefir.toStream(person,".name"); // Every time `.name` changes, increase the count 1. const nameChangeCountStream = nameStream.scan(function(lastValue) { return lastValue + 1; }, 0); // Log the current nameChangeStream value nameChangeStream.onValue(function(newValue) { console.log(newValue); }); person.name = "Ramiya" // logs 1 person.name = "Payal" // logs 2The
toStreammethod can take an observable object and a property (or event) and create an event stream. The following creates a stream of theperson.nameproperty values:const person = new can.DefineMap({name: "Justin"}); const nameStream = can.streamKefir.toStream(person,".name"); nameStream.onValue(function(newValue) { console.log(newValue); }); person.name = "Ramiya" // logs "Ramiya" person.name = "Payal" // logs "Payal"Kefir’s map method can be used to convert event-stream values into new values. The following creates an event stream of upper-cased names:
const person = new can.DefineMap({name: "Justin"}); const capitalizedNameStream = can.streamKefir.toStream(person,".name") .map(function(name) { return name.toUpperCase() }); nameStream.onValue(function(newValue) { console.log(newValue); }); person.name = "Ramiya" // logs "RAMIYA" person.name = "Payal" // logs "PAYAL"The can-define-stream-kefir module lets you define a property value using a stream. For example, we can define a
nameChangeCountproperty of aPersontype usingstreamlike:Person = can.DefineMap.extend({ name: "string", nameChangeCount: { stream: function() { return this.toStream(".name").scan(function(lastValue) { return lastValue + 1; }, 0); } } }); can.defineStreamKefir(Person);Notice that the can-define-stream-kefir module is used as a mixin. When called on a type (like
Person), the mixin looks for PropDefinitions withstreamproperty definition functions. It uses the stream instance returned by thestreamproperty definition function as the value of the property.Stream properties, like asynchronous getters, only have a value when bound to. To read the
nameChangeCount, first use.onlike:const me = new Person({name: "Justin"}); me.on("nameChangeCount", function(ev, newValue) { console.log(newValue); }); me.nameChangeCount //-> 0 me.name = "Ramiya" // logs 1 me.nameChangeCount //-> 1The
streamproperty definition function is passedsetStreamwhich is a stream of values set on the property. The following allows a user to setnameChangeCountto reset the count at some new value:Person = can.DefineMap.extend({ name: "string", nameChangeCount: { stream: function(setStream) { const reset = setStream.map(function(value) { return {type: "reset", value: value}; }); const increment = this.toStream(".name").map(function() { return {type: "increment"} }); return reset.merge(increment).scan(function(lastValue, next) { if (next.type === "increment") { return lastValue + 1; } else { return next.value; } }, 0); } } }); can.defineStreamKefir(Person);The following shows the behavior of this property:
const me = new Person({name: "Justin"}); me.on("nameChangeCount", function(ev, newValue) { console.log(newValue); }); me.nameChangeCount = 10; me.name = "Ramiya" // logs 11 me.nameChangeCount //-> 11The can-define-stream-kefir module adds a
map.toStreammethod which is an alias forcanStream.toStream. Use it to create streams from properties and events on a map instance like:const Person = can.DefineMap.extend({ name: "string" }); const me = new Person({name: "Justin"}); const nameStream = me.toStream(".name"); nameStream.onValue(function() { ... })
The solution
Update the JavaScript tab to:
- Remove the setter side-effects from
location. - Change
placeto derive its value from:- changes in
location-placeshould benulliflocationchanges. - the
.placesvalue -placeshould be the one and only place inplacesif there is only one place inplaces. - the set
.placevalue.
- changes in
- Mix can-define-stream-kefir into the
WeatherViewModel.
const yqlURL = "https://query.yahooapis.com/v1/public/yql?";
const WeatherViewModel = can.DefineMap.extend({
location: "string",
get placesPromise() {
if (this.location && this.location.length > 2) {
return fetch(
yqlURL +
can.param({
q: 'select * from geo.places where text="' + this.location + '"',
format: "json"
})
).then(function(response) {
return response.json();
}).then(function(data) {
console.log(data);
if (Array.isArray(data.query.results.place)) {
return data.query.results.place;
} else {
return [data.query.results.place];
}
});
}
},
places: {
get: function(lastSet, resolve) {
if (this.placesPromise) {
this.placesPromise.then(resolve);
}
}
},
get showPlacePicker() {
return !this.place && this.places && this.places.length > 1;
},
place: {
stream: function(setStream) {
const resetStream = this.toStream(".location").map(function() {
return null;
});
const onePlaceResultStream = this.toStream(".places").map(function(places) {
if (places.length === 1) {
return places[0];
} else {
return null;
}
});
return onePlaceResultStream
.merge(setStream)
.merge(resetStream);
}
},
pickPlace: function(place) {
this.place = place;
},
get forecastPromise() {
if (this.place) {
console.log("place", this.place);
return fetch(
yqlURL +
can.param({
q: 'select * from weather.forecast where woeid=' + this.place.woeid,
format: "json"
})
).then(function(response) {
return response.json();
}).then(function(data) {
console.log("forecast data", data);
const forecast = data.query.results.channel.item.forecast;
return forecast;
});
}
},
toClassName: function(text) {
return text.toLowerCase().replace(/ /g, "-");
}
});
can.defineStreamKefir(WeatherViewModel);
const vm = new WeatherViewModel();
const template = can.stache.from("app-template");
const fragment = template(vm);
document.body.appendChild(fragment);
Get the geoLocation’s latitude and longitude
The problem
Instead of requiring the user to search for their city, let’s change the app to use the browser’s geolocation API to look up their location. For this step, we will add the following behaviors:
- If the user enables location services, we will write their latitude and longitude.
- If the user disables location services or there is some other type of error, we will print the error message.
We will do this by:
- Creating a Kefir stream of the User’s position or error messages.
- Using that stream to create the
geoLocationandgeoLocationErrorproperties. - Displaying the data of those properties in the template.
What you need to know
The geolocation API allows you to request the user’s position as follows:
navigator.geolocation.getCurrentPosition( function(position) {...}, function(err) {...});The geolocation API allows you to monitor changes in the user’s position as follows:
const watch = navigator.geolocation.watchPosition( function(position) {...}, function(err) {...});To cancel watching, call:
navigator.geolocation.clearWatch(watch);To create a
Kefirstream, callKefir.streamas follows:const myStream = Kefir.stream(function setup(emitter) { // INITIALIZATION CODE return function teardown() { // TEARDOWN CODE } });Kefir.streamis passed an event emitter which can emit values like:emitter.value(123);or errors like:
emitter.error("something went wrong");or end the stream of values like:
emitter.end();Typically, you listen to sources and emit values in the
setupfunction and stop listening to sources in theteardownfunction. For example, the following might listen to where the user’s mouse is on the page:const cursorPosition = Kefir.stream(function(emitter) { const handler = function(ev) { emitter.emit({pageX: ev.pageX, pageY: pageY}); }; document.documentElement.addEventListener("mousemove",handler); return function() { document.documentElement.removeEventListener("mousemove",handler); } })Kefir’s
stream.withHandler( handler(emitter, event) )is able to convert one stream’s events to another stream. All other stream methods likestream.mapandstream.scancan be implemented withstream.withHandler. For example, the following maps thecursorPositionstream to acursorDistancestream:cursorDistance = cursorPosition.withHandler(function(emitter, event) { if (event.type === "end") { emitter.end(); } if (event.type === "error") { emitter.error(event.value); } if (event.type === "value") { const pageX = event.value.pageX; const pageY = event.value.pageY; emitter.value( Math.sqrt(pageX*pageX + pageY*pageY) ); } });Notice how
withHandleris called with the emitter ofcursorDistanceand the events ofcursorPosition.
The solution
Update the JavaScript tab:
const yqlURL = "https://query.yahooapis.com/v1/public/yql?";
const geoLocationStream = Kefir.stream(function(emitter) {
navigator.geolocation.getCurrentPosition(function(position) {
emitter.value(position);
}, function(err) {
console.log("getCurrentPositionErr",err);
emitter.error(err);
});
const watch = navigator.geolocation.watchPosition(function(position) {
emitter.value(position);
}, function(err) {
emitter.error(err);
});
return function() {
navigator.geolocation.clearWatch(watch);
};
});
const WeatherViewModel = can.DefineMap.extend({
geoLocation: {
stream: function() {
return geoLocationStream;
}
},
geoLocationError: {
stream: function() {
return geoLocationStream.withHandler(function(emitter, event) {
if (event.type === "end") {
emitter.end();
}
if (event.type === "error") {
emitter.value(event.value);
}
});
}
},
location: "string",
get placesPromise() {
if (this.location && this.location.length > 2) {
return fetch(
yqlURL +
can.param({
q: 'select * from geo.places where text="' + this.location + '"',
format: "json"
})
).then(function(response) {
return response.json();
}).then(function(data) {
console.log(data);
if (Array.isArray(data.query.results.place)) {
return data.query.results.place;
} else {
return [data.query.results.place];
}
});
}
},
places: {
get: function(lastSet, resolve) {
if (this.placesPromise) {
this.placesPromise.then(resolve);
}
}
},
get showPlacePicker() {
return !this.place && this.places && this.places.length > 1;
},
place: {
stream: function(setStream) {
const resetStream = this.toStream(".location").map(function() {
return null;
});
const onePlaceResultStream = this.toStream(".places").map(function(places) {
if (places.length === 1) {
return places[0];
} else {
return null;
}
});
return onePlaceResultStream
.merge(setStream)
.merge(resetStream);
}
},
pickPlace: function(place) {
this.place = place;
},
get forecastPromise() {
if (this.place) {
console.log("place", this.place);
return fetch(
yqlURL +
can.param({
q: 'select * from weather.forecast where woeid=' + this.place.woeid,
format: "json"
})
).then(function(response) {
return response.json();
}).then(function(data) {
console.log("forecast data", data);
const forecast = data.query.results.channel.item.forecast;
return forecast;
});
}
},
toClassName: function(text) {
return text.toLowerCase().replace(/ /g, "-");
}
});
can.defineStreamKefir(WeatherViewModel);
const vm = new WeatherViewModel();
const template = can.stache.from("app-template");
const fragment = template(vm);
document.body.appendChild(fragment);
Update the HTML tab:
Latitude: {{geoLocation.coords.latitude}},
Longitude: {{geoLocation.coords.longitude}},
Error: {{geoLocationError.message}}
<div class="weather-widget">
<div class="location-entry">
<label for="location">Enter Your location:</label>
<input id="location" value:to="location" type="text" />
</div>
{{#if(placesPromise.isPending)}}
<p class="loading-message">
Loading places…
</p>
{{/if}}
{{#if(showPlacePicker)}}
<div class="location-options">
<label>Pick your place:</label>
<ul>
{{#each(placesPromise.value)}}
<li on:click="../pickPlace(this)">{{name}}, {{admin1.content}},
{{country.code}} ({{placeTypeName.content}})</li>
{{/each}}
</ul>
</div>
{{/if}}
{{#if(place)}}
<div class="forecast">
<h1>10 day {{place.name}} Weather Forecast</h1>
<ul>
{{#each(forecastPromise.value)}}
<li>
<span class="date">{{date}}</span>
<span class="description {{toClassName(text)}}">{{text}}</span>
<span class="high-temp">{{high}}<sup>°</sup></span>
<span class="low-temp">{{low}}<sup>°</sup></span>
</li>
{{/each}}
</ul>
</div>
{{/if}}
</div>
Find the user’s place by latitude and longitude
The problem
We need to get which place the user is in by their
latitude and longitude. We will save this place as the
geoPlace property and use it in the place property definition.
What you need to know
Flickr has an API that can get a place that is recognized by
Yahoo’s weather APIs. It can be retrieved with fetch like:
fetch("https://api.flickr.com/services/rest/?"+
can.param({
method: "flickr.places.findByLatLon",
api_key: "df0a221bb43ecbc2abb03426bd84e598",
lat: LATITUDE,
lon: LONGITUDE,
format: "json",
nojsoncallback: 1
})
).then(function(response) {
return response.json()
}).then(function(responseJSON) {
return responseJSON.places.place[0];
});
The solution
Update the JavaScript tab:
const yqlURL = "https://query.yahooapis.com/v1/public/yql?";
const geoLocationStream = Kefir.stream(function(emitter) {
navigator.geolocation.getCurrentPosition(function(position) {
emitter.value(position);
}, function(err) {
console.log("getCurrentPositionErr",err);
emitter.error(err);
});
const watch = navigator.geolocation.watchPosition(function(position) {
emitter.value(position);
}, function(err) {
emitter.error(err);
});
return function() {
navigator.geolocation.clearWatch(watch);
};
});
const WeatherViewModel = can.DefineMap.extend({
geoLocation: {
stream: function() {
return geoLocationStream;
}
},
geoLocationError: {
stream: function() {
return geoLocationStream.withHandler(function(emitter, event) {
if (event.type === "end") {
emitter.end();
}
if (event.type === "error") {
emitter.value(event.value);
}
});
}
},
geoPlace: {
get: function(lastSet, resolve) {
if (this.geoLocation) {
fetch("https://api.flickr.com/services/rest/?" +
can.param({
method: "flickr.places.findByLatLon",
api_key: "df0a221bb43ecbc2abb03426bd84e598",
lat: this.geoLocation.coords.latitude,
lon: this.geoLocation.coords.longitude,
format: "json",
nojsoncallback: 1
})
).then(function(response) {
return response.json();
}).then(function(responseJSON) {
return responseJSON.places.place[0];
}).then(resolve);
}
}
},
location: "string",
get placesPromise() {
if (this.location && this.location.length > 2) {
return fetch(
yqlURL +
can.param({
q: 'select * from geo.places where text="' + this.location + '"',
format: "json"
})
).then(function(response) {
return response.json();
}).then(function(data) {
console.log(data);
if (Array.isArray(data.query.results.place)) {
return data.query.results.place;
} else {
return [data.query.results.place];
}
});
}
},
places: {
get: function(lastSet, resolve) {
if (this.placesPromise) {
this.placesPromise.then(resolve);
}
}
},
get showPlacePicker() {
return !this.place && this.places && this.places.length > 1;
},
place: {
stream: function(setStream) {
const resetStream = this.toStream(".location").map(function() {
return null;
});
const onePlaceResultStream = this.toStream(".places").map(function(places) {
if (places.length === 1) {
return places[0];
} else {
return null;
}
});
return onePlaceResultStream
.merge(setStream)
.merge(resetStream)
.merge(this.toStream(".geoPlace"));
}
},
pickPlace: function(place) {
this.place = place;
},
get forecastPromise() {
if (this.place) {
console.log("place", this.place);
return fetch(
yqlURL +
can.param({
q: 'select * from weather.forecast where woeid=' + this.place.woeid,
format: "json"
})
).then(function(response) {
return response.json();
}).then(function(data) {
console.log("forecast data", data);
const forecast = data.query.results.channel.item.forecast;
return forecast;
});
}
},
toClassName: function(text) {
return text.toLowerCase().replace(/ /g, "-");
}
});
can.defineStreamKefir(WeatherViewModel);
const vm = new WeatherViewModel();
const template = can.stache.from("app-template");
const fragment = template(vm);
document.body.appendChild(fragment);
Add "Enable Location Services" message
The problem
When a user first views the page, they will be prompted to enable location
services. While they are prompted, we will display a Please Enable Location Services… message.
What you need to know
Display the message while geoLocation and geoLocationError are undefined.
The solution
Update the JavaScript tab:
const yqlURL = "https://query.yahooapis.com/v1/public/yql?";
const geoLocationStream = Kefir.stream(function(emitter) {
navigator.geolocation.getCurrentPosition(function(position) {
emitter.value(position);
}, function(err) {
console.log("getCurrentPositionErr",err);
emitter.error(err);
});
const watch = navigator.geolocation.watchPosition(function(position) {
emitter.value(position);
}, function(err) {
emitter.error(err);
});
return function() {
navigator.geolocation.clearWatch(watch);
};
});
const WeatherViewModel = can.DefineMap.extend({
geoLocation: {
stream: function() {
return geoLocationStream;
}
},
geoLocationError: {
stream: function() {
return geoLocationStream.withHandler(function(emitter, event) {
if (event.type === "end") {
emitter.end();
}
if (event.type === "error") {
emitter.value(event.value);
}
});
}
},
geoPlace: {
get: function(lastSet, resolve) {
if (this.geoLocation) {
fetch("https://api.flickr.com/services/rest/?" +
can.param({
method: "flickr.places.findByLatLon",
api_key: "df0a221bb43ecbc2abb03426bd84e598",
lat: this.geoLocation.coords.latitude,
lon: this.geoLocation.coords.longitude,
format: "json",
nojsoncallback: 1
})
).then(function(response) {
return response.json();
}).then(function(responseJSON) {
return responseJSON.places.place[0];
}).then(resolve);
}
}
},
get showEnableGeoLocationMessage() {
return !this.geoLocation && !this.geoLocationError;
},
location: "string",
get placesPromise() {
if (this.location && this.location.length > 2) {
return fetch(
yqlURL +
can.param({
q: 'select * from geo.places where text="' + this.location + '"',
format: "json"
})
).then(function(response) {
return response.json();
}).then(function(data) {
console.log(data);
if (Array.isArray(data.query.results.place)) {
return data.query.results.place;
} else {
return [data.query.results.place];
}
});
}
},
places: {
get: function(lastSet, resolve) {
if (this.placesPromise) {
this.placesPromise.then(resolve);
}
}
},
get showPlacePicker() {
return !this.place && this.places && this.places.length > 1;
},
place: {
stream: function(setStream) {
const resetStream = this.toStream(".location").map(function() {
return null;
});
const onePlaceResultStream = this.toStream(".places").map(function(places) {
if (places.length === 1) {
return places[0];
} else {
return null;
}
});
return onePlaceResultStream
.merge(setStream)
.merge(resetStream)
.merge(this.toStream(".geoPlace"));
}
},
pickPlace: function(place) {
this.place = place;
},
get forecastPromise() {
if (this.place) {
console.log("place", this.place);
return fetch(
yqlURL +
can.param({
q: 'select * from weather.forecast where woeid=' + this.place.woeid,
format: "json"
})
).then(function(response) {
return response.json();
}).then(function(data) {
console.log("forecast data", data);
const forecast = data.query.results.channel.item.forecast;
return forecast;
});
}
},
toClassName: function(text) {
return text.toLowerCase().replace(/ /g, "-");
}
});
can.defineStreamKefir(WeatherViewModel);
const vm = new WeatherViewModel();
const template = can.stache.from("app-template");
const fragment = template(vm);
document.body.appendChild(fragment);
Update the HTML tab:
Latitude: {{geoLocation.coords.latitude}},
Longitude: {{geoLocation.coords.longitude}},
Error: {{geoLocationError.message}}
<div class="weather-widget">
{{#if(showEnableGeoLocationMessage)}}
<p class="loading-message">
Please Enable Location Services…
</p>
{{/if}}
<div class="location-entry">
<label for="location">Enter Your location:</label>
<input id="location" value:to="location" type="text" />
</div>
{{#if(placesPromise.isPending)}}
<p class="loading-message">
Loading places…
</p>
{{/if}}
{{#if(showPlacePicker)}}
<div class="location-options">
<label>Pick your place:</label>
<ul>
{{#each(placesPromise.value)}}
<li on:click="../pickPlace(this)">{{name}}, {{admin1.content}},
{{country.code}} ({{placeTypeName.content}})</li>
{{/each}}
</ul>
</div>
{{/if}}
{{#if(place)}}
<div class="forecast">
<h1>10 day {{place.name}} Weather Forecast</h1>
<ul>
{{#each(forecastPromise.value)}}
<li>
<span class="date">{{date}}</span>
<span class="description {{toClassName(text)}}">{{text}}</span>
<span class="high-temp">{{high}}<sup>°</sup></span>
<span class="low-temp">{{low}}<sup>°</sup></span>
</li>
{{/each}}
</ul>
</div>
{{/if}}
</div>
Allow user to enter location only if location services failed
The problem
Show the location entry <div> only when geo location has failed.
What you need to know
Nothing, you’ve learned it all by this point. Apply what you know!
The solution
Update the JavaScript tab:
const yqlURL = "https://query.yahooapis.com/v1/public/yql?";
const geoLocationStream = Kefir.stream(function(emitter) {
navigator.geolocation.getCurrentPosition(function(position) {
emitter.value(position);
}, function(err) {
console.log("getCurrentPositionErr",err);
emitter.error(err);
});
const watch = navigator.geolocation.watchPosition(function(position) {
emitter.value(position);
}, function(err) {
emitter.error(err);
});
return function() {
navigator.geolocation.clearWatch(watch);
};
});
const WeatherViewModel = can.DefineMap.extend({
geoLocation: {
stream: function() {
return geoLocationStream;
}
},
geoLocationError: {
stream: function() {
return geoLocationStream.withHandler(function(emitter, event) {
if (event.type === "end") {
emitter.end();
}
if (event.type === "error") {
emitter.value(event.value);
}
});
}
},
geoPlace: {
get: function(lastSet, resolve) {
if (this.geoLocation) {
fetch("https://api.flickr.com/services/rest/?" +
can.param({
method: "flickr.places.findByLatLon",
api_key: "df0a221bb43ecbc2abb03426bd84e598",
lat: this.geoLocation.coords.latitude,
lon: this.geoLocation.coords.longitude,
format: "json",
nojsoncallback: 1
})
).then(function(response) {
return response.json();
}).then(function(responseJSON) {
return responseJSON.places.place[0];
}).then(resolve);
}
}
},
get showEnableGeoLocationMessage() {
return !this.geoLocation && !this.geoLocationError;
},
get showEnterLocation() {
return !!this.geoLocationError;
},
location: "string",
get placesPromise() {
if (this.location && this.location.length > 2) {
return fetch(
yqlURL +
can.param({
q: 'select * from geo.places where text="' + this.location + '"',
format: "json"
})
).then(function(response) {
return response.json();
}).then(function(data) {
console.log(data);
if (Array.isArray(data.query.results.place)) {
return data.query.results.place;
} else {
return [data.query.results.place];
}
});
}
},
places: {
get: function(lastSet, resolve) {
if (this.placesPromise) {
this.placesPromise.then(resolve);
}
}
},
get showPlacePicker() {
return !this.place && this.places && this.places.length > 1;
},
place: {
stream: function(setStream) {
const resetStream = this.toStream(".location").map(function() {
return null;
});
const onePlaceResultStream = this.toStream(".places").map(function(places) {
if (places.length === 1) {
return places[0];
} else {
return null;
}
});
return onePlaceResultStream
.merge(setStream)
.merge(resetStream)
.merge(this.toStream(".geoPlace"));
}
},
pickPlace: function(place) {
this.place = place;
},
get forecastPromise() {
if (this.place) {
console.log("place", this.place);
return fetch(
yqlURL +
can.param({
q: 'select * from weather.forecast where woeid=' + this.place.woeid,
format: "json"
})
).then(function(response) {
return response.json();
}).then(function(data) {
console.log("forecast data", data);
const forecast = data.query.results.channel.item.forecast;
return forecast;
});
}
},
toClassName: function(text) {
return text.toLowerCase().replace(/ /g, "-");
}
});
can.defineStreamKefir(WeatherViewModel);
const vm = new WeatherViewModel();
const template = can.stache.from("app-template");
const fragment = template(vm);
document.body.appendChild(fragment);
Update the HTML tab:
Latitude: {{geoLocation.coords.latitude}},
Longitude: {{geoLocation.coords.longitude}},
Error: {{geoLocationError.message}}
<div class="weather-widget">
{{#if(showEnableGeoLocationMessage)}}
<p class="loading-message">
Please Enable Location Services…
</p>
{{/if}}
{{#if(showEnterLocation)}}
<div class="location-entry">
<label for="location">Enter Your location:</label>
<input id="location" value:to="location" type="text" />
</div>
{{/if}}
{{#if(placesPromise.isPending)}}
<p class="loading-message">
Loading places…
</p>
{{/if}}
{{#if(showPlacePicker)}}
<div class="location-options">
<label>Pick your place:</label>
<ul>
{{#each(placesPromise.value)}}
<li on:click="../pickPlace(this)">{{name}}, {{admin1.content}},
{{country.code}} ({{placeTypeName.content}})</li>
{{/each}}
</ul>
</div>
{{/if}}
{{#if(place)}}
<div class="forecast">
<h1>10 day {{place.name}} Weather Forecast</h1>
<ul>
{{#each(forecastPromise.value)}}
<li>
<span class="date">{{date}}</span>
<span class="description {{toClassName(text)}}">{{text}}</span>
<span class="high-temp">{{high}}<sup>°</sup></span>
<span class="low-temp">{{low}}<sup>°</sup></span>
</li>
{{/each}}
</ul>
</div>
{{/if}}
</div>
Result
When finished, you should see something like the following JS Bin: