diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
index c834fa22..0712cd67 100644
--- a/.github/FUNDING.yml
+++ b/.github/FUNDING.yml
@@ -1 +1,2 @@
+github: plotly
 custom: https://plotly.com/products/consulting-and-oem/
diff --git a/.gitignore b/.gitignore
index e6c7bbd5..3f2b95ac 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,4 @@ node_modules/
 python/
 todo.txt
 r-finance*
+build/
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 99df24ef..2f019a22 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
 
 ## [Unreleased]
 ### Added
+- Pattern-matching IDs and callbacks. Component IDs can be lists, and callbacks can reference patterns of components, using three different wildcards: `ALL`, `MATCH`, and `ALLSMALLER`. This lets you create components on demand, and have callbacks respond to any and all of them. To help with this, `app$callback_context` gets three new entries: `outputs_list`, `inputs_list`, and `states_list`, which contain all the ids, properties, and except for the outputs, the property values from all matched components. [#228](https://github.com/plotly/dashR/pull/228)
 - New and improved callback graph in the debug menu. Now based on Cytoscape for much more interactivity, plus callback profiling including number of calls, fine-grained time information, bytes sent and received, and more. You can even add custom timing information on the server with `callback_context.record_timing(name, duration, description)` [#224](https://github.com/plotly/dashR/pull/224)
 - Support for setting attributes on `external_scripts` and `external_stylesheets`, and validation for the parameters passed (attributes are verified, and elements that are lists themselves must be named). [#226](https://github.com/plotly/dashR/pull/226)
 - Dash for R now supports user-defined routes and redirects via the `app$server_route` and `app$redirect` methods. [#225](https://github.com/plotly/dashR/pull/225)
diff --git a/NAMESPACE b/NAMESPACE
index 8ac7711a..aeae30d4 100644
--- a/NAMESPACE
+++ b/NAMESPACE
@@ -1,7 +1,10 @@
 # Generated by roxygen2: do not edit by hand
 
 S3method(print,dash_component)
+export(ALL)
+export(ALLSMALLER)
 export(Dash)
+export(MATCH)
 export(clientsideFunction)
 export(dashNoUpdate)
 export(input)
diff --git a/R/dash.R b/R/dash.R
index e7b059ca..e7928541 100644
--- a/R/dash.R
+++ b/R/dash.R
@@ -221,18 +221,38 @@ Dash <- R6::R6Class(
         callback_args <- list()
 
         for (input_element in request$body$inputs) {
-          if(is.null(input_element$value))
+          if (any(grepl("id.", names(unlist(input_element))))) {
+            if (!is.null(input_element$id)) input_element <- list(input_element)
+            values <- character(0)
+            for (wildcard_input in input_element) {
+              values <- c(values, wildcard_input$value)
+            }
+            callback_args <- c(callback_args, ifelse(length(values), list(values), list(NULL)))
+          }
+          else if(is.null(input_element$value)) {
             callback_args <- c(callback_args, list(list(NULL)))
-          else
+          }
+          else {
             callback_args <- c(callback_args, list(input_element$value))
+          }
         }
 
         if (length(request$body$state)) {
           for (state_element in request$body$state) {
-            if(is.null(state_element$value))
+            if (any(grepl("id.", names(unlist(state_element))))) {
+              if (!is.null(state_element$id)) state_element <- list(state_element)
+              values <- character(0)
+              for (wildcard_state in state_element) {
+                values <- c(values, wildcard_state$value)
+              }
+              callback_args <- c(callback_args, ifelse(length(values), list(values), list(NULL)))
+            }
+            else if(is.null(state_element$value)) {
               callback_args <- c(callback_args, list(list(NULL)))
-            else
+            }
+            else {
               callback_args <- c(callback_args, list(state_element$value))
+            }
           }
         }
 
@@ -290,6 +310,12 @@ Dash <- R6::R6Class(
              response = allprops,
              multi = TRUE
              )
+          } else if (is.list(request$body$outputs$id)) {
+            props = setNames(list(output_value), gsub( "(^.+)(\\.)", "", request$body$output))
+            resp <- list(
+              response = setNames(list(props), to_JSON(request$body$outputs$id)),
+              multi = TRUE
+            )
           } else {
             resp <- list(
               response = list(
@@ -770,12 +796,43 @@ Dash <- R6::R6Class(
     #' containing valid JavaScript, or a call to [clientsideFunction],
     #' including `namespace` and `function_name` arguments for a locally served
     #' JavaScript function.
+    #'
+    #'
+    #' For pattern-matching callbacks, the `id` field of a component is written
+    #' in JSON-like syntax which describes a dictionary object when serialized
+    #' for consumption by the Dash renderer. The fields are arbitrary keys
+    #' , which describe the targets of the callback.
+    #'
+    #' For example, when we write `input(id=list("foo" = ALL, "bar" = "dropdown")`,
+    #' Dash interprets this as "match any input that has an ID list where 'foo'
+    #' is 'ALL' and 'bar' is anything." If any of the dropdown
+    #' `value` properties change, all of their values are returned to the callback.
+    #'
+    #' However, for readability, we recommend using keys like type, index, or id.
+    #' `type` can be used to refer to the class or set of dynamic components and
+    #' `index` or `id` could be used to refer to the component you are matching
+    #' within that set. While your application may have a single set of dynamic
+    #' components, it's possible to specify multiple sets of dynamic components
+    #' in more complex apps or if you are using `MATCH`.
+    #'
+    #' Like `ALL`, `MATCH` will fire the callback when any of the component's properties
+    #' change. However, instead of passing all of the values into the callback, `MATCH`
+    #' will pass just a single value into the callback. Instead of updating a single
+    #' output, it will update the dynamic output that is "matched" with.
+    #'
+    #' `ALLSMALLER` is used to pass in the values of all of the targeted components
+    #' on the page that have an index smaller than the index corresponding to the div.
+    #' For example, `ALLSMALLER` makes it possible to filter results that are
+    #' increasingly specific as the user applies each additional selection.
+    #'
+    #' `ALLSMALLER` can only be used in `input` and `state` items, and must be used
+    #' on a key that has `MATCH` in the `output` item(s). `ALLSMALLER` it isn't always
+    #' necessary (you can usually use `ALL` and filter out the indices in your callback),
+    #' but it will make your logic simpler.
     callback = function(output, params, func) {
       assert_valid_callbacks(output, params, func)
-
       inputs <- params[vapply(params, function(x) 'input' %in% attr(x, "class"), FUN.VALUE=logical(1))]
       state <- params[vapply(params, function(x) 'state' %in% attr(x, "class"), FUN.VALUE=logical(1))]
-
       if (is.function(func)) {
         clientside_function <- NULL
       } else if (is.character(func)) {
diff --git a/R/dependencies.R b/R/dependencies.R
index 77ec8b77..66951fc2 100644
--- a/R/dependencies.R
+++ b/R/dependencies.R
@@ -1,5 +1,15 @@
 # akin to https://github.com/plotly/dash/blob/d2ebc837/dash/dependencies.py
 
+# Helper functions for handling dependency ids or props
+setWildcardId <- function(id) {
+  # Sort the keys of a wildcard id
+  id <- id[order(names(id))]
+  all_selectors <- vapply(id, function(x) {is.symbol(x)}, logical(1))
+  id[all_selectors] <- as.character(id[all_selectors])
+  id[!all_selectors] <- lapply(id[!all_selectors], function(x) {jsonlite::unbox(x)})
+  return(as.character(jsonlite::toJSON(id, auto_unbox = FALSE)))
+}
+
 #' Input/Output/State definitions
 #'
 #' Use in conjunction with the `callback()` method from the [dash::Dash] class
@@ -8,13 +18,23 @@
 #' The `dashNoUpdate()` function permits application developers to prevent a
 #' single output from updating the layout. It has no formal arguments.
 #' 
+#' `ALL`, `ALLSMALLER` and `MATCH` are symbols corresponding to the
+#' pattern-matching callback selectors with the same names. These allow you
+#' to write callbacks that respond to or update an arbitrary or dynamic
+#' number of components. For more information, see the `callback` section
+#' in \link{dash}.
+#'
 #' @name dependencies
 #' @param id a component id
 #' @param property the component property to use
 
 #' @rdname dependencies
 #' @export
+
 output <- function(id, property) {
+  if (is.list(id)) {
+    id = setWildcardId(id)
+  }
   structure(
     dependency(id, property),
     class = c("dash_dependency", "output")
@@ -24,6 +44,9 @@ output <- function(id, property) {
 #' @rdname dependencies
 #' @export
 input <- function(id, property) {
+  if (is.list(id)) {
+    id = setWildcardId(id)
+  }
   structure(
     dependency(id, property),
     class = c("dash_dependency", "input")
@@ -33,6 +56,9 @@ input <- function(id, property) {
 #' @rdname dependencies
 #' @export
 state <- function(id, property) {
+  if (is.list(id)) {
+    id = setWildcardId(id)
+  }
   structure(
     dependency(id, property),
     class = c("dash_dependency", "state")
@@ -41,6 +67,9 @@ state <- function(id, property) {
 
 dependency <- function(id = NULL, property = NULL) {
   if (is.null(id)) stop("Must specify an id", call. = FALSE)
+  if (is.list(id)) {
+    id = setWildcardId(id)
+  }
   list(
     id = id,
     property = property
@@ -54,3 +83,15 @@ dashNoUpdate <- function() {
   class(x) <- "no_update"
   return(x)
 }
+
+#' @rdname dependencies
+#' @export
+ALL <- as.symbol("ALL")
+
+#' @rdname dependencies
+#' @export
+ALLSMALLER <- as.symbol("ALLSMALLER")
+
+#' @rdname dependencies
+#' @export
+MATCH <- as.symbol("MATCH")
diff --git a/R/utils.R b/R/utils.R
index 92d6973a..74cbf2f4 100644
--- a/R/utils.R
+++ b/R/utils.R
@@ -290,6 +290,22 @@ assert_no_names <- function (x)
                paste(nms, collapse = "', '")), call. = FALSE)
 }
 
+assertValidWildcards <- function(dependency) {
+  if (is.symbol(dependency$id)) {
+    result <- (jsonlite::validate(as.character(dependency$id)) && grepl("{", dependency$id))
+  } else {
+    result <- TRUE
+  }
+  if (!result) {
+    dependencyType <- class(dependency)
+    stop(sprintf("A callback %s ID contains restricted pattern matching callback selectors ALL, MATCH or ALLSMALLER. Please verify that it is formatted as a pattern matching callback list ID, or choose a different component ID.",
+                 dependencyType[dependencyType %in% c("input", "output", "state")]),
+         call. = FALSE)
+  } else {
+    return(result)
+  }
+}
+
 # the following function attempts to prune remote CSS
 # or local CSS/JS dependencies that either should not
 # be resolved to local R package paths, or which have
@@ -403,6 +419,27 @@ assert_valid_callbacks <- function(output, params, func) {
     stop(sprintf("The callback method requires that one or more properly formatted inputs are passed."), call. = FALSE)
   }
 
+  # Verify that 'input', 'state' and 'output' parameters only contain 'Wildcard' keywords if they are JSON formatted ids for pattern matching callbacks
+  valid_wildcard_inputs <- sapply(inputs, function(x) {
+    assertValidWildcards(x)
+  })
+  
+  
+  valid_wildcard_state <- sapply(state, function(x) {
+    assertValidWildcards(x)
+  })
+  
+  if(any(sapply(output, is.list))) {
+    valid_wildcard_output <- sapply(output, function(x) {
+      assertValidWildcards(x)
+    })
+  } else {
+    valid_wildcard_output <- sapply(list(output), function(x) {
+      assertValidWildcards(x)
+    })
+  }
+
+  
   # Check that outputs are not inputs
   # https://github.com/plotly/dash/issues/323
 
@@ -987,9 +1024,23 @@ removeHandlers <- function(fnList) {
 }
 
 setCallbackContext <- function(callback_elements) {
-  states <- lapply(callback_elements$states, function(x) {
-    setNames(x$value, paste(x$id, x$property, sep="."))
-  })
+  # Set state elements for this callback
+  
+  if (length(callback_elements$state[[1]]) == 0) {
+    states <- sapply(callback_elements$state, function(x) {
+      setNames(list(x$value), paste(x$id, x$property, sep="."))
+    })
+  } else if (is.character(callback_elements$state[[1]][[1]])) {
+    states <- sapply(callback_elements$state, function(x) {
+      setNames(list(x$value), paste(x$id, x$property, sep="."))
+    })
+  } else {
+    states <- sapply(callback_elements$state, function(x) {
+      states_vector <- unlist(x)
+      setNames(list(states_vector[grepl("value|value.", names(states_vector))]), 
+               paste(as.character(jsonlite::toJSON(x[[1]])), x$property, sep="."))
+    })
+  }
 
   splitIdProp <- function(x) unlist(strsplit(x, split = "[.]"))
 
@@ -997,19 +1048,54 @@ setCallbackContext <- function(callback_elements) {
                       function(x) {
                         input_id <- splitIdProp(x)[1]
                         prop <- splitIdProp(x)[2]
-
-                        id_match <- vapply(callback_elements$inputs, function(x) x$id %in% input_id, logical(1))
-                        prop_match <- vapply(callback_elements$inputs, function(x) x$property %in% prop, logical(1))
-
-                        value <- sapply(callback_elements$inputs[id_match & prop_match], `[[`, "value")
-
-                        list(`prop_id` = x, `value` = value)
+                        
+                        # The following conditionals check whether the callback is a pattern-matching callback and if it has been triggered. 
+                        if (startsWith(input_id, "{")){
+                          id_match <- vapply(callback_elements$inputs, function(x) {
+                            x <- unlist(x)
+                            any(x[grepl("id.", names(x))] %in% jsonlite::fromJSON(input_id)[[1]])
+                          }, logical(1))[[1]]
+                        } else {
+                          id_match <- vapply(callback_elements$inputs, function(x) x$id %in% input_id, logical(1))
+                        }
+                        
+                        if (startsWith(input_id, "{")){
+                          prop_match <- vapply(callback_elements$inputs, function(x) {
+                            x <- unlist(x)
+                            any(x[names(x) == "property"] %in% prop)
+                          }, logical(1))[[1]]
+                        } else {
+                          prop_match <- vapply(callback_elements$inputs, function(x) x$property %in% prop, logical(1))
+                        }
+                        
+                        if (startsWith(input_id, "{")){
+                          if (length(callback_elements$inputs) == 1 || !is.null(unlist(callback_elements$inputs, recursive = F)$value)) {
+                            value <- sapply(callback_elements$inputs[id_match & prop_match], `[[`, "value")
+                          } else {
+                            value <- sapply(callback_elements$inputs[id_match & prop_match][[1]], `[[`, "value")
+                          }
+                        } else {
+                          value <- sapply(callback_elements$inputs[id_match & prop_match], `[[`, "value")
+                        }
+                        
+                        return(list(`prop_id` = x, `value` = value))
                       }
-                      )
-
-  inputs <- sapply(callback_elements$inputs, function(x) {
-    setNames(list(x$value), paste(x$id, x$property, sep="."))
-  })
+                    )
+  if (length(callback_elements$inputs[[1]]) == 0 || is.character(callback_elements$inputs[[1]][[1]])) {
+    inputs <- sapply(callback_elements$inputs, function(x) {
+      setNames(list(x$value), paste(x$id, x$property, sep="."))
+    })
+  } else if (length(callback_elements$inputs[[1]]) > 1) {
+    inputs <- sapply(callback_elements$inputs, function(x) {
+      inputs_vector <- unlist(x)
+      setNames(list(inputs_vector[grepl("value|value.", names(inputs_vector))]), paste(as.character(jsonlite::toJSON(x$id)), x$property, sep="."))
+    })
+  } else {
+    inputs <- sapply(callback_elements$inputs, function(x) {
+      inputs_vector <- unlist(x)
+      setNames(list(inputs_vector[grepl("value|value.", names(inputs_vector))]), paste(as.character(jsonlite::toJSON(x[[1]]$id)), x[[1]]$property, sep="."))
+    })
+  }
 
   return(list(states=states,
               triggered=unlist(triggered, recursive=FALSE),
diff --git a/man/Dash.Rd b/man/Dash.Rd
index d8210223..acab9892 100644
--- a/man/Dash.Rd
+++ b/man/Dash.Rd
@@ -487,7 +487,39 @@ with its own defined \code{id} and \code{property}.}
 arguments. \code{func} may be any valid R function, or a character string
 containing valid JavaScript, or a call to \link{clientsideFunction},
 including \code{namespace} and \code{function_name} arguments for a locally served
-JavaScript function.}
+JavaScript function.
+
+For pattern-matching callbacks, the \code{id} field of a component is written
+in JSON-like syntax which describes a dictionary object when serialized
+for consumption by the Dash renderer. The fields include a \code{type} and
+an \code{index}, which describe the targets of the callback.
+
+For example, when we write \verb{input(id=list("index" = ALL, "type" = "filter-dropdown")},
+Dash interprets this as "match any input that has an ID list where 'type'
+is 'filter-dropdown' and 'index' is anything." If any of the dropdown
+\code{value} properties change, all of their values are returned to the callback.
+
+However, for readability, we recommend using keys like type, index, or id.
+\code{type} can be used to refer to the class or set of dynamic components and
+\code{index} or \code{id} could be used to refer which component you are matching
+within that set. While your applicatoin may have a single set of dynamic
+components, it's possible to specify multiple sets of dynamic components
+in more complex apps or if you are using \code{MATCH}.
+
+Like \code{ALL}, \code{MATCH} will fire the callback when any of the component's properties
+change. However, instead of passing all of the values into the callback, \code{MATCH}
+will pass just a single value into the callback. Instead of updating a single
+output, it will update the dynamic output that is "matched" with.
+
+\code{ALLSMALLER} is used to pass in the values of all of the targeted components
+on the page that have an index smaller than the index corresponding to the div.
+For example, \code{ALLSMALLER} makes it possible to filter results that are
+increasingly specific as the user applies each additional selection.
+
+\code{ALLSMALLER} can only be used in \code{input} and \code{state} items, and must be used
+on a key that has \code{MATCH} in the \code{output} item(s). \code{ALLSMALLER} it isn't always
+necessary (you can usually use \code{ALL} and filter out the indices in your callback),
+but it will make your logic simpler.}
 }
 \if{html}{\out{</div>}}
 }
diff --git a/man/dependencies.Rd b/man/dependencies.Rd
index 44f83721..86af65cc 100644
--- a/man/dependencies.Rd
+++ b/man/dependencies.Rd
@@ -1,12 +1,23 @@
 % Generated by roxygen2: do not edit by hand
 % Please edit documentation in R/dependencies.R
+\docType{data}
 \name{dependencies}
 \alias{dependencies}
 \alias{output}
 \alias{input}
 \alias{state}
 \alias{dashNoUpdate}
+\alias{ALL}
+\alias{ALLSMALLER}
+\alias{MATCH}
 \title{Input/Output/State definitions}
+\format{
+An object of class \code{name} of length 1.
+
+An object of class \code{name} of length 1.
+
+An object of class \code{name} of length 1.
+}
 \usage{
 output(id, property)
 
@@ -15,6 +26,12 @@ input(id, property)
 state(id, property)
 
 dashNoUpdate()
+
+ALL
+
+ALLSMALLER
+
+MATCH
 }
 \arguments{
 \item{id}{a component id}
@@ -28,4 +45,11 @@ to define the update logic in your application.
 \details{
 The \code{dashNoUpdate()} function permits application developers to prevent a
 single output from updating the layout. It has no formal arguments.
+
+\code{ALL}, \code{ALLSMALLER} and \code{MATCH} are symbols corresponding to the
+pattern-matching callback selectors with the same names. These allow you
+to write callbacks that respond to or update an arbitrary or dynamic
+number of components. For more information, see the \code{callback} section
+in \link{dash}.
 }
+\keyword{datasets}
diff --git a/tests/integration/callbacks/test_pattern_matching.py b/tests/integration/callbacks/test_pattern_matching.py
new file mode 100644
index 00000000..e96b44e8
--- /dev/null
+++ b/tests/integration/callbacks/test_pattern_matching.py
@@ -0,0 +1,377 @@
+all_app = """
+library(dash)
+library(dashHtmlComponents)
+library(dashCoreComponents)
+
+app <- Dash$new()
+
+app$layout(htmlDiv(list(
+  htmlButton("Add Filter", id="add-filter", n_clicks=0),
+  htmlDiv(id="dropdown-container", children=list()),
+  htmlDiv(id="dropdown-container-output")
+)))
+
+
+app$callback(
+  output(id="dropdown-container", property = "children"),
+  params = list(
+    input(id = "add-filter", property = "n_clicks"),
+    state(id = "dropdown-container", property = "children")
+  ),
+  display_dropdowns <- function(n_clicks, children){
+    new_dropdown = dccDropdown(
+      id=list(
+        "bar" = n_clicks,
+        "foo" = "filter-dropdown"
+      ),
+      options = lapply(c("NYC", "MTL", "LA", "TOKYO"), function(x){
+        list("label" = x, "value" = x)
+      })
+    )
+    children[[n_clicks + 1]] <- new_dropdown
+    return(children)
+  }
+)
+
+
+app$callback(
+  output(id="dropdown-container-output", property="children"),
+  params = list(
+    input(id=list("bar" = ALL, "foo" = "filter-dropdown"), property= "value")
+  ),
+  display_output <- function(test){
+    ctx <- app$callback_context()
+    return(htmlDiv(
+      lapply(1:length(test), function(x){
+        return(htmlDiv(sprintf("Dropdown %s = %s", x, test[[x]])))
+      })
+    ))
+  }
+)
+
+
+app$run_server()
+"""
+
+allsmaller_app = """
+library(dash)
+library(dashCoreComponents)
+library(dashHtmlComponents)
+
+
+df <- read.csv('https://raw.githubusercontent.com/plotly/datasets/master/gapminder2007.csv', stringsAsFactors = FALSE)
+
+
+app <- Dash$new()
+
+app$layout(htmlDiv(list(
+  htmlButton("Add Filter", id = "add-filter-ex3", n_clicks = 0),
+  htmlDiv(id = "container-ex3", children = list())
+)))
+
+
+app$callback(
+  output('container-ex3', 'children'),
+  params = list(
+    input('add-filter-ex3', 'n_clicks'),
+    state('container-ex3', 'children')
+  ),
+  display_dropdowns <- function(n_clicks, existing_children){
+    new_children <- htmlDiv(list(
+      dccDropdown(
+        id = list("index" = n_clicks, "type" = "filter-dropdown-ex3"),
+        options = lapply(unique(df$country), function(x){
+          list("label" = x, "value" = x)
+        }),
+        value = unique(df$country)[n_clicks + 1]
+      ),
+      htmlDiv(id = list("index" = n_clicks, "type" = "output-ex3"), children = list(unique(df$country)[n_clicks + 1]))
+    ))
+    
+    existing_children <- c(existing_children, list(new_children))
+  }
+)
+
+
+app$callback(
+  output(id = list("type" = "output-ex3", "index" = MATCH), property = "children"),
+  params = list(
+    input(id = list("type" = "filter-dropdown-ex3", "index" = MATCH), property = "value"),
+    input(id = list("type" = "filter-dropdown-ex3", "index" = ALLSMALLER), property = "value")
+  ),
+  display_output <- function(matching_value, previous_values){
+    previous_values_in_reversed_order = rev(previous_values)
+    all_values = c(matching_value, previous_values_in_reversed_order)
+    all_values = unlist(all_values)
+    
+    dff = df[df$country %in% all_values,]
+    avgLifeExp = round(mean(dff$lifeExp), digits = 2)
+    
+    if (length(all_values) == 1) {
+      return(
+        htmlDiv(sprintf("%s is the life expectancy of %s.", avgLifeExp, matching_value))
+      )
+    } else if (length(all_values) == 2) {
+      return(
+        htmlDiv(sprintf("%s is the life expectancy of %s.", avgLifeExp, paste(all_values, collapse = " and ")),
+         id="test")
+      )
+    } else {
+      return(
+        htmlDiv(sprintf("%s is the life expectancy of %s, and %s.", avgLifeExp, paste(all_values[-length(all_values)], 
+        collapse = " , "), paste(all_values[length(all_values)])))
+      )
+    }
+  }
+)
+
+app$run_server()
+"""
+
+match_app = """
+library(dash)
+library(dashCoreComponents)
+library(dashHtmlComponents)
+
+
+app <- Dash$new()
+
+app$layout(htmlDiv(list(
+  htmlButton("Add Filter", id="dynamic-add-filter", n_clicks=0),
+  htmlDiv(id="dynamic-dropdown-container", children = list())
+)))
+
+
+app$callback(
+  output(id="dynamic-dropdown-container", "children"),
+  params = list(
+    input("dynamic-add-filter", "n_clicks"),
+    state("dynamic-dropdown-container", "children")
+  ),
+  display_dropdown <- function(n_clicks, children){
+    new_element = htmlDiv(list(
+      dccDropdown(
+        id = list("index" = n_clicks, "type" = "dynamic-dropdown"),
+        options = lapply(c("NYC", "MTL", "LA", "TOKYO"), function(x){
+          list("label" = x, "value" = x)
+        })
+      ),
+      htmlDiv(
+        id = list("index" = n_clicks, "type" = "dynamic-output"),
+        children = list()
+      )
+    ))
+
+    children <- c(children, list(new_element))
+    return(children)
+  }
+)
+
+
+app$callback(
+  output(id = list("index" = MATCH, "type" = "dynamic-output"), property= "children"),
+  params = list(
+    input(id=list("index" = MATCH, "type" = "dynamic-dropdown"), property= "value"),
+    state(id=list("index" = MATCH, "type" = "dynamic-dropdown"), property= "id")
+  ),
+  display_output <- function(value, id){
+    return(htmlDiv(sprintf("Dropdown %s = %s", id$index, value)))
+  }
+)
+
+app$run_server()
+"""
+
+todo_app = """
+library(dash)
+library(dashCoreComponents)
+library(dashHtmlComponents)
+
+app <- Dash$new()
+
+
+app$layout(htmlDiv(list(
+  htmlDiv('Dash To-Do List'),
+  dccInput(id = 'new-item'),
+  htmlButton("Add", id = "add"),
+  htmlButton("Clear Done", id = "clear-done"),
+  htmlDiv(id = "list-container"),
+  htmlDiv(id = "totals")
+)))
+
+
+style_todo <- list("display" = "inline", "margin" = "10px")
+style_done <- list("display" = "inline", "margin" = "10px", "textDecoration" = "line-through", "color" = "#888")
+
+
+app$callback(
+  output = list(
+    output("list-container", "children"),
+    output("new-item", "value")
+  ),
+  params = list(
+    input("add", "n_clicks"),
+    input("new-item", "n_submit"),
+    input("clear-done", "n_clicks"),
+    state("new-item", "value"),
+    state(list("index" = ALL, "type" = "check"), "children"),
+    state(list("index" = ALL, "type" = "done"), "value")
+  ),
+  edit_list <- function(add, add2, clear, new_item, items, items_done) {
+    ctx <- app$callback_context()
+    triggered <- ifelse(is.null(ctx$triggered$prop_id), " ", ctx$triggered$prop_id)
+    adding <- grepl(triggered, "add.n_clicks|new-item.n_submit")
+    clearing = grepl(triggered, "clear-done.n_clicks")
+
+    # Create a list which we will hydrate with "items" and "items_done"
+    new_spec <- list()
+    for (i in seq_along(items)) {
+      if (!is.null(items[[i]])) {
+        new_spec[[length(new_spec) + 1]] <- list(items[[i]], list())
+      }
+    }
+
+    for (i in seq_along(items_done)) {
+      if (!is.null(items_done[[i]])) {
+        new_spec[[i]][[2]] <- items_done[[i]]
+      }
+    }
+
+    # If clearing, we remove elements from the list which have been marked "done"
+    if (clearing) {
+      remove_vector <- c()
+      for (i in seq_along(new_spec)) {
+        if (length(new_spec[[i]][[2]]) > 0) {
+          remove_vector <- c(remove_vector, i)
+        }
+      }
+      new_spec <- new_spec[-remove_vector]
+    }
+
+    # Add a new item to the list
+    if (adding) {
+      new_spec[[length(new_spec) + 1]] <- list(new_item, list())
+    }
+
+    # Generate dynamic components with pattern matching IDs
+    new_list <- list()
+    if (!is.null(unlist(new_spec))) {
+      for (i in seq_along(new_spec)) {
+        add_list <- list(htmlDiv(list(
+          dccChecklist(
+            id = list("index" = i, "type" = "done"),
+            options = list(
+              list("label" = "", "value" = "done")
+            ),
+            value = new_spec[[i]][[2]],
+            style = list("display" = "inline"),
+            labelStyle = list("display" = "inline")
+          ),
+          htmlDiv(new_spec[[i]][[1]], id = list("index" = i, "type" = "check"), style = if (length(new_spec[[i]][[2]]) == 0) style_todo else style_done)
+        ), style = list("clear" = "both")))
+        new_list <- c(new_list, add_list)
+      }
+      return(list(new_list, ""))
+    } else {
+      return(list(list(), ""))
+    }
+  }
+)
+
+app$callback(
+  output(id = list("index" = MATCH, "type" = "check"), property = "style"),
+  params = list(
+    input(id = list("index" = MATCH, "type" = "done"), property = "value")
+  ),
+  mark_done <- function(done){
+    if (length(done[[1]] > 0)) return(style_done) else return(style_todo)
+  }
+)
+
+
+app$callback(
+  output(id = "totals", property = "children"),
+  params = list(
+    input(list("index" = ALL, "type" = "done"), property = "value"),
+    state(list("index" = ALL, "type" = "check"), property = "children")
+  ),
+  show_totals <- function(done, total) {
+    count_all = length(total)
+    count_done = length(done)
+
+    result = sprintf("%s of %s items completed", count_done, count_all)
+
+    if (count_all > 0) {
+      result = paste(result, sprintf(" - %s%%", as.integer(100 * count_done/count_all)))
+    }
+
+    if (is.null(total[[1]])) {
+      return("Add an item to the list to get started.")
+    } else {
+      return(result)
+    }
+  }
+)
+
+
+app$run_server()
+"""
+
+
+def test_rpmc001_pattern_matching_all(dashr):
+    dashr.start_server(all_app)
+    dashr.find_element("#add-filter").click()
+    dashr.select_dcc_dropdown('#\\{\\"bar\\"\\:1\\,\\"foo\\"\\:\\"filter-dropdown\\"\\}', "NYC")
+    dashr.wait_for_text_to_equal(
+        "#dropdown-container-output",
+        "Dropdown 1 = NYC"
+    )
+    dashr.find_element("#add-filter").click()
+    dashr.select_dcc_dropdown('#\\{\\"bar\\"\\:2\\,\\"foo\\"\\:\\"filter-dropdown\\"\\}', "MTL")
+    dashr.wait_for_text_to_equal(
+        "#dropdown-container-output",
+        "Dropdown 1 = NYC"
+        "\n"
+        "Dropdown 2 = MTL"
+    )
+
+
+def test_rpmc002_pattern_matching_allsmaller(dashr):
+    dashr.start_server(allsmaller_app)
+    dashr.find_element("#add-filter-ex3").click()
+    dashr.select_dcc_dropdown('#\\{\\"index\\"\\:1\\,\\"type\\"\\:\\"filter-dropdown-ex3\\"\\}', "Argentina")
+    dashr.find_element("#add-filter-ex3").click()
+    dashr.select_dcc_dropdown('#\\{\\"index\\"\\:2\\,\\"type\\"\\:\\"filter-dropdown-ex3\\"\\}', "Angola")
+    dashr.wait_for_text_to_equal(
+        "#test",
+        "59.03 is the life expectancy of Angola and Argentina."
+    )
+
+
+def test_rpmc003_pattern_matching_match(dashr):
+    dashr.start_server(match_app)
+    dashr.find_element("#dynamic-add-filter").click()
+    dashr.select_dcc_dropdown('#\\{\\"index\\"\\:1\\,\\"type\\"\\:\\"dynamic-dropdown\\"\\}', "NYC")
+    dashr.wait_for_text_to_equal(
+        '#\\{\\"index\\"\\:1\\,\\"type\\"\\:\\"dynamic-output\\"\\}',
+        "Dropdown 1 = NYC"
+    )
+    dashr.find_element("#dynamic-add-filter").click()
+    dashr.select_dcc_dropdown('#\\{\\"index\\"\\:2\\,\\"type\\"\\:\\"dynamic-dropdown\\"\\}', "MTL")
+    dashr.wait_for_text_to_equal(
+        '#\\{\\"index\\"\\:2\\,\\"type\\"\\:\\"dynamic-output\\"\\}',
+        "Dropdown 2 = MTL"
+    )
+
+
+def test_rpmc004_pattern_matching_todo(dashr):
+    dashr.start_server(todo_app)
+    dashr.find_element("#new-item").send_keys("Item 1")
+    dashr.find_element("#add").click()
+    dashr.find_element("#new-item").send_keys("Item 2")
+    dashr.find_element("#add").click()
+    dashr.find_element("#new-item").send_keys("Item 3")
+    dashr.find_element("#add").click()
+    dashr.find_element('#\\{\\"index\\"\\:1\\,\\"type\\"\\:\\"done\\"\\}').click()
+    dashr.find_element("#clear-done").click()
+    dashr.wait_for_text_to_equal("#totals", "0 of 2 items completed - 0%")
diff --git a/tests/testthat/test-wildcards.R b/tests/testthat/test-wildcards.R
index 6d1be1ec..3e7f1daf 100644
--- a/tests/testthat/test-wildcards.R
+++ b/tests/testthat/test-wildcards.R
@@ -24,5 +24,6 @@ test_that("HTML `data-*` & `aria-* ` wildcards are passed along to layout approp
   expect_equal(x$props$`data-foo`, 1)
 })
 
+
 # TODO: test NULL values aren't rendered on the HTML div
 # https://github.com/plotly/dash/pull/237/files#r179251041