DoneJS StealJS jQuery++ FuncUnit DocumentJS
4.3.0
5.0.0 3.13.1 2.3.35
  • About
  • Guides
  • API Docs
  • Community
  • Contributing
  • Bitovi
    • Bitovi.com
    • Blog
    • Design
    • Development
    • Training
    • Open Source
    • About
    • Contact Us
  • About
  • Guides
    • experiment
      • Chat Guide
      • TodoMVC Guide
      • ATM Guide
    • getting started
      • Setting Up CanJS
      • Technology Overview
      • Reading the Docs (API Guide)
      • Experimental ES Module Usage
    • recipes
      • Canvas Clock (Simple)
      • Credit Card Guide (Advanced)
      • Credit Card Guide (Simple)
      • CTA Bus Map (Medium)
      • File Navigator Guide (Advanced)
      • File Navigator Guide (Simple)
      • Playlist Editor (Advanced)
      • Signup and Login (Simple)
      • Text Editor (Medium)
      • Tinder Carousel (Medium)
      • TodoMVC with StealJS
      • Video Player (Simple)
    • topics
      • Debugging
      • Forms
    • upgrade
      • Migrating to CanJS 3
      • Migrating to CanJS 4
      • Using Codemods
  • API Docs
  • Community
  • Contributing
  • GitHub
  • Twitter
  • Chat
  • Forum
  • News
Bitovi

Weather Report Guide (Advanced)

  • Edit on GitHub

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:

JS Bin on jsbin.com

Start this tutorial by cloning the following JS Bin:

JS Bin on jsbin.com

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 following person map’s name 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 the person.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 a Person type using stream 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 with stream property definition functions. It uses the stream instance returned by the stream 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 passed setStream which is a stream of values set on the property. The following allows a user to set nameChangeCount 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 for canStream.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:

  1. Remove the setter side-effects from location.
  2. Change place to derive its value from:
    • changes in location - place should be null if location changes.
    • the .places value - place should be the one and only place in places if there is only one place in places.
    • the set .place value.
  3. 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 and geoLocationError 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, call Kefir.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 the teardown 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 like stream.map and stream.scan can be implemented with stream.withHandler. For example, the following maps the cursorPosition stream to a cursorDistance 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 of cursorDistance and the events of cursorPosition.

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>&deg;</sup></span>
            <span class="low-temp">{{low}}<sup>&deg;</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>&deg;</sup></span>
            <span class="low-temp">{{low}}<sup>&deg;</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>&deg;</sup></span>
            <span class="low-temp">{{low}}<sup>&deg;</sup></span>
          </li>
        {{/each}}
      </ul>
    </div>
  {{/if}}

</div>

Result

When finished, you should see something like the following JS Bin:

JS Bin on jsbin.com

CanJS is part of DoneJS. Created and maintained by the core DoneJS team and Bitovi. Currently 4.3.0.

On this page

Get help

  • Chat with us
  • File an issue
  • Ask questions
  • Read latest news