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
DefineMap
getters 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
Kefir
stream that counts the number of times the followingperson
map’sname
property 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 2
The
toStream
method can take an observable object and a property (or event) and create an event stream. The following creates a stream of theperson.name
property 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
nameChangeCount
property of aPerson
type usingstream
like: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 withstream
property definition functions. It uses the stream instance returned by thestream
property 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.on
like: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 //-> 1
The
stream
property definition function is passedsetStream
which is a stream of values set on the property. The following allows a user to setnameChangeCount
to 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 //-> 11
The can-define-stream-kefir module adds a
map.toStream
method 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
place
to derive its value from:- changes in
location
-place
should benull
iflocation
changes. - the
.places
value -place
should be the one and only place inplaces
if there is only one place inplaces
. - the set
.place
value.
- 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
geoLocation
andgeoLocationError
properties. - 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
Kefir
stream, callKefir.stream
as follows:const myStream = Kefir.stream(function setup(emitter) { // INITIALIZATION CODE return function teardown() { // TEARDOWN CODE } });
Kefir.stream
is 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
setup
function and stop listening to sources in theteardown
function. 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.map
andstream.scan
can be implemented withstream.withHandler
. For example, the following maps thecursorPosition
stream to acursorDistance
stream: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
withHandler
is called with the emitter ofcursorDistance
and 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: