Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URL's added here will be added as <link>s in order, and before the CSS in the editor. If you link to another Pen, it will include the CSS from that Pen. If the preprocessor matches, it will attempt to combine them before processing.

+ add another resource

JavaScript

Babel includes JSX processing.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Packages

Add Packages

Search for and use JavaScript packages from npm here. By selecting a package, an import statement will be added to the top of the JavaScript editor for this package.

Behavior

Save Automatically?

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

              
                <div class="content">
  <h1>RxJS Unsplash Search</h1>

  <p>
    This is a demo of an <a href="http://unsplash.com">Unsplash</a> search widget inspired by Trello.
    It was developed as a proof of concept for using RxJS.
    Features include infinite scroll and http cancellation.
    It is currently using mock data, but can easily be changed to use the Unsplash API.
  </p>

  <div class="unsplash-search unsplash-search-clearfix">
    <input type="text" class="unsplash-search-input">
    <span class="unsplash-search-close">X</span>
    <div class="unsplash-search-dropdown">
    </div>
    <div class="unsplash-search-loading unsplash-search-hidden">Loading...</div>
  </div>
</div>
              
            
!

CSS

              
                * {
  margin: 0;
  padding: 0;
}

body {
  font-family: Arial, Helvetica, sans-serif;
}

h1 {
  margin: 20px 0;
  font-size: 28px;
}

p {
  margin: 20px 0;
}

.content {
  width: 350px;
  margin: auto;
}

.unsplash-search {
  width: 300px;
  background: #eee;
  padding: 0;
}

.unsplash-search-input {
  outline: none;
  width: 258px;
  margin: 4px;
  padding: 4px;
  border: 0;
  border: 1px solid #ccc;
}

.unsplash-search-dropdown {
  overflow: auto;
  max-height: 350px
}

.unsplash-search-thumb {
  border-radius: 5px;
  width: 132px;
  height: 100px;
  margin: 5px 5px;
  float: left;
  background-position: center;
  background-repeat: none;
  background-size: cover;
  position: relative;
  cursor: pointer;
}

.unsplash-search-attribution {
  visibility: hidden;
  text-decoration: none;
}

.unsplash-search-thumb:hover .unsplash-search-attribution {
  position: absolute;
  bottom: 0;
  display: block;
  width: 122px;
  padding: 5px;
  background: #333;
  color: #fff;
  opacity: 0.5;
  visibility: visible;
}

.unsplash-search-thumb a:hover {
  text-decoration: underline;
}

.unsplash-search-loading {
 text-align: center;
 margin: 20px;
}

.unsplash-search-hidden {
  display: none;
}

.unsplash-search-close {
  cursor: pointer;
  position: relative;
  top: 1px;
}

.unsplash-search-clearfix:after {
  content: "";
  display: table;
  clear: both;
}

              
            
!

JS

              
                // RxJS Unsplash Search
// Author: Travis Luong

// to use this with unsplash, you'll have to change mock to false and add your unsplash client id
var config = {
  mock: true,
  mockUrl: 'https://api.myjson.com/bins/linzj',
  unsplashClientId: '',
}

// get all the elements
var input = document.querySelector('.unsplash-search-input')
var dropdown = document.querySelector('.unsplash-search-dropdown')
var loading = document.querySelector('.unsplash-search-loading')
var close = document.querySelector('.unsplash-search-close')

// create observables from events
var input$ = Rx.Observable.fromEvent(input, 'input')
var dropdownClick$ = Rx.Observable.fromEvent(dropdown, 'click')
var dropdownScrollBot$ = Rx.Observable.fromEvent(dropdown, 'scroll')
var closeClick$ = Rx.Observable.fromEvent(close, 'click')

// app state
var images = []
var currentPage = 0
var currentQuery = ''
var isLoading = false

// the input stream that emits an event whenever user changes the input field
input$
  // this do block updates the global state and resets the ui
  // also shows or hides the loading indicator based on whether the input is empty
  .do(e => {
    images = []
    renderData(images)
    currentPage = 0
    currentQuery = e.target.value
    if (e.target.value.length === 0) {
      loading.classList.add('unsplash-search-hidden')
    } else {
      loading.classList.remove('unsplash-search-hidden')
    }
  })
  // debounce the input 500ms so that only the last value is emitted
  .debounceTime(500)
  // only pass value down the chain if length of value is equal to or greater than 1
  .filter(e => e.target.value.length >= 1)
  // returns a new observable which uses the emitted value
  // switchMap will cancel the previous observable whenever a new value is emitted
  // if the previous http request is still in progress when a user enters a new value, the previous request will be canceled
  .switchMap(e => {
    if (config.mock) {
      return getMockData()
    } else {
      return getData(e.target.value, currentPage)
    }
  })
  // get the results from the results and pass it down the chain
  .map(e => e.response.results)
  // subscribe to this observable and handle the data
  .subscribe(data => {
    // only render the data if the input is not empty
    if (currentQuery != '') {
      // cache the images so that we can append more to it later for the infinite scroll
      images = data
      renderData(data)
    }
    // hide rid of the loading indicator
    loading.classList.add('unsplash-search-hidden')
  })

// how we handle the click events on the images
dropdownClick$
  // event delegation on dropdown by filtering for the thumb class
  .filter(e => e.target.classList.contains('unsplash-search-thumb'))
  // get the image id from the data-image-id attribute
  .map(e => e.target.dataset.imageId)
  // get the image object from the global images array
  .map(imageId => images.find((image) => image.id === imageId))
  // handle the click here
  .subscribe(image => console.log(image))

// emits an event whenever scroll happens within dropdown
dropdownScrollBot$
  // calculation to filter the scroll is near the bottom
  .filter(e => e.target.scrollTop + e.target.clientHeight >= e.target.scrollHeight - 100)
  // if dropdown is already loading, do not pass event down the chain
  .filter(e => !isLoading)
  // do not pass event down the chain if the input is blank
  .filter(e => currentQuery.length >= 1)
  // mutate some state
  .do(e => {
    isLoading = true
    currentPage += 1
    loading.classList.remove('unsplash-search-hidden')
  })
  // mergeMap is kind of like switchMap but it doesn't cancel previous observable on emit
  // we don't have to worry about cancellation here since isLoading will protect us
  .mergeMap(e => {
    if (config.mock) {
      return getMockData()
    } else {
      return getData(currentQuery, currentPage)
    }
  })
  // get the results from response
  .map(e => e.response.results)
  // handle that data and mutate some state
  .subscribe(data => {
    images = images.concat(data)
    renderData(images)
    loading.classList.add('unsplash-search-hidden')
    isLoading = false
  })

// clicking the x button will clear everything out
closeClick$
  .subscribe(e => {
    currentQuery = ''
    input.value = ''
    isLoading = false
    loading.classList.add('unsplash-search-hidden')
    images = []
    renderData(images)
  })

// generate the html using the images data
function renderData (data) {
  // reduce is another functional thing that lets you accumulate stuff
  var html = data.reduce((acc, image) => {
    acc += '<div class="unsplash-search-thumb" style="background: url(' + image.urls.thumb + ')" data-image-id="' + image.id + '">'
      + '<a class="unsplash-search-attribution" href="' + image.user.links.html + '">' + image.user.name + '</a>'
      + '</div>'
    return acc
  }, '')

  dropdown.innerHTML = html
}

// mock data observable
// we are using myjson.com to serve up static json
// i also have a simple express server to serve up delayed json to test http cancellation
function getMockData () {
  return Rx.Observable.ajax({
    url: config.mockUrl
  })
}

// actual ajax for unsplash api
// you'll need to add your unsplash client id to the config object above and turn off mock to use this
function getData (query, page) {
  return Rx.Observable.ajax({
    url: 'https://api.unsplash.com/search/photos?query=' + query + '&page=' + page + '&per_page=10',
    headers: {
      authorization: 'Client-ID ' + config.unsplashClientId
    }
  })
}

              
            
!
999px

Console