# streamlit-notification_center-component

Streamlit component that listens to postMessage() traffic with a certain format.

## Installation instructions

```sh
pip install streamlit_notification_center_component
```

## Usage instructions

```python
import streamlit as st
from streamlit_notification_center_component import notification_center

notification_center(key="globalnc")
```

## Helper JavaScript methods

```js
function ncSendMessage(target, body) {
  Array.from(window.parent.frames).forEach((frame) => {
    frame.postMessage({ type: 'nc', target: target, payload: body }, "*")
  })
}

function ncClearTarget(target) {
  Array.from(window.parent.frames).forEach((frame) => {
    frame.postMessage({ type: 'nc', target: target, command: 'clear' }, "*")
  })
}

function ncClearAll() {
  Array.from(window.parent.frames).forEach((frame) => {
    frame.postMessage({ type: 'nc', command: 'clear-all' }, "*")
  })
}
```

Let's talk about what's going on here. Each of these three functions loop over all frames in the parent and invoke a postMessage on them. The `type: 'nc'` code MUST be present or the notification_center widget will ignore the message.

In cases where clearing the data from state on the streamlit side is desired, a `command:` property needs to be included. There are two possible values:

 * **'clear'**: when present, a `target:` property must also be included to identify what to clear
 * **'clear-all'**: when present, no other value than `type: 'nc'` needs be present. All messages stored in the `st.session_state._nc_data` dict will be removed.

 Lastly, when it comes to a normal message to be queued, the way this works is that streamlit will receive the value from the component's call to `Streamlit.setComponentValue()`. This will be a pre-vetted object conforming to the following TypeScript type:

 ```ts
 export type NotificationMessage = {
  type: 'nc';
  target?: string;
  payload?: any;
  from?: string;
  command?: null | 'clear' | 'clear-all';
}
```

When not sending a command to clear and `type`, `target` and `payload` are all present, then the dict `st.session_state._nc_data` will have its key `.setdefault(target, [])`. This ensures that there is at least an empty array of values for the supplied target. Then the payload object is appended to this array. All streamlit code can check for and use values. 

## Helper Python Methods
These streamlit python functions are also defined

```python
from streamlit_notification_center_component import nc_get, nc_get_last, nc_get_all, nc_set, nc_has, nc_clear, nc_clear_all

def nc_get(key, default_value=None):
  """
  Retrieves the value associated with a given key from the session state.
  
  Args:
      key (str): The key for the value to retrieve.
      default_value (optional): The value to return if the key is not found. Defaults to None.

  Returns:
      The value associated with the key if found, or the default value if the key is not found.
  """
  if '_nc_data' not in st.session_state:
    st.session_state._nc_data = {}

  if st.session_state._nc_data.get(key) != None:
    return st.session_state._nc_data[key]
  else:
    return default_value
    
def nc_get_last(key, default_value=None):
  """
  Retrieves the value associated with a given key from the session state.
  
  Args:
      key (str): The key representing an array of messages with tis target
      default_value (optional): The value to return if the key is not found. Defaults to None.

  Returns:
      The last value for the specified `key` rather than the whole array. If no
      value is forthcoming, `default_value` is returned instead.
  """
  if '_nc_data' not in st.session_state:
    st.session_state._nc_data = {}

  if st.session_state._nc_data.get(key) != None:
    channel = st.session_state._nc_data.get(key)
    if channel:
      return channel[-1]
    else:
      return default_value 
  else:
    return default_value

def nc_get_all():
  """
  Retrieves all key-value pairs stored in the session state.
  
  Returns:
      dict: A dictionary containing all key-value pairs in the session state.
  """
  return st.session_state._nc_data

def nc_set(key, value):
  """
  Stores a value in the session state, associated with a given key.
  
  Args:
      key (str): The key to associate with the value.
      value: The value to store.
  """
  channel = st.session_state.setdefault('_nc_data', {}).setdefault(key, [])
  channel.append(value)

def nc_has(key):
  """
  Checks whether a given key exists in the session state.
  
  Args:
      key (str): The key to check for existence.

  Returns:
      bool: True if the key exists, False otherwise.
  """  
  return [False, True][nc_get(key) != None]

def nc_clear(key):
  """
  Clears the value associated with a given key in the session state.
  
  Args:
      key (str): The key for the value to clear.
  """  
  nc_get(key, []).clear()

def nc_clear_all():
  """
  Clears all key-value pairs stored in the session state.
  """
  st.session_state._nc_data = {}
```

## Usage Examples

Okay, down to brass tacks. The `streamlit_notification_center_component`'s primary purpose is to allow code in other custom components to send data back to streamlit in one shared location or to allow HTML in a call to `st.components.v1.html` to communicate back to the Python streamlit side of things. Currently layout is very limited in streamlit from an HTML perspective. Being able to write a quick layout using something like a table or set of inline-block divs is doable using the `st.components.v1.html` approach. However as soon as you have an action or other feature that needs to send data back to streamlit, you've moved into custom component territory.

### Working with `st.components.v1.html`

Let's take the most common example first. If you wanted to introduce the following HTML with a stylized link that looks like a button

````python
notification_center(key="globalnc")
st.components.v1.html(
  f'''
    <a class="stylized-button" onclick="sendData()">Button</a>
    <style>
      a.stylized-button {{
        padding: 0.25em;
        margin: 0.1em;
        border: 1px solid slategrey;
        background-color: lightslategray;
        border-radius: 5px;
        color: lightyellow;
        cursor: pointer;
        display: inline-block;
        user-select: none;        
      }}
    </style>
    <script>
      function sendData() {{
        ncSendMessage('stylized-button', {{msg: 'Stylized Button Pressed'}})
      }}

      function ncSendMessage(target, body) {{
        Array.from(window.parent.frames).forEach((frame) => {{
          frame.postMessage({{ type: 'nc', target: target, payload: body }}, "*")
        }})
      }}      
    </script>
  '''
)
````

What's going on here? Well, first off we are creating a new instance of the `notification_center()` component. The key supplied can be anything but will positively identify this NotificationCenter instance from another should there be one. If debug printing is enabled (see below) then you'll see "[NotificationCenter(key)][target] value" strings in the python logs. Note that key will be the `key=` value supplied when the `notification_center()` component is created, `target` will be the key the array of messages will be appended to and `value` will be the array of values for that `target`.

Then we are injecting into the page a block of HTML with some CSS and JavaScript. You'll note in this example that the `ncSendMessage` function from above has been included in the block. This is the easiest way to handle sending a `postMessage()` call to all frames in the parent of the iframe this code will reside in. The `NotificationCenter` component will be residing in another iframe with how streamlit works. 

We now have the ability to inject custom HTML that can speak back to streamlit into your streamlit pages. Something that was not provided by default behavior.

### Working with custom components

Other components should be able to import `notification_center` and use the `nc-` prefixed python functions for communicating with NotificationCenter data. As components send messages to the NotificationCenter, those values can be checked on the python side and can be worked with.

### Debug messaging

If you're running into trouble, turn on debug messaging. This is a runtime variable that is set in

```python
st.session_state.setdefault('_nc_debug', False)
```

To turn it on, simply change the value to True

```python
st.session_state._nc_debug = True
```

### Visual examples

![Screenshot 2023-08-16 at 2 25 06 PM](https://github.com/cafemedia/streamlit_notification_center_component/assets/225558/643b1557-9a2c-49cb-9182-f1ddb28d23b4)


After this point you'll see NotificationCenter messages appearing in your Python console.
