streamlit-notification_center-component
Streamlit component that listens to postMessage() traffic with a certain format.
Installation instructions
pip install streamlit_notification_center_component
Testing instructions
If you wish to test the component outside of another project, open two shells. In one, you'll want to run the react code in one shell and the python __init__.py
in another. However, before you do this you need to open __init__.py
first and change the flag _RELEASE
from True
to False
or you'll get no output.
In shell no.1
cd notification_center/frontend
npm install
npm start
In shell no.2
source venv/bin/activate
streamlit run notification_center/__init__.py
If all went well, you'll see a window open up with some text and some buttons.
Usage instructions
import streamlit as st
from streamlit_notification_center_component import notification_center
notification_center(key="globalnc")
Helper JavaScript methods
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:
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
In addition to providing the streamlit component for use, the component comes with several useful python functions that make working with the component much easier. These functions are obviously provided to aid on the streamlit/python side of things.
nc_ensure()
The nc_ensure()
function is provided to ensure that the environment is setup properly for use. It is heavily used by the internal functions and can be used in your own scripts. For example, if you wish to setup your use of notification_center()
in a separate file, you might do something like this:
from notification_center import nc_ensure, nc_add_style
def nc_setup():
nc_ensure()
nc_add_style(f'* {{font-size:200%;}}')
This function would ensure that any poking or prodding to st.session_state._nc_
variables would be ready to go if this is something you needed to do.
nc_get(key, default_value=None)
The nc_get()
function allows the retrieval of the array of messages sent to the key in question. If no value exists with the supplied key then the default_value, defaulting to None, is what is returned.
let button_presses = nc_get('button_clicks')
if button_presses:
The value returned here will be a list or array of zero or more values sent to the key
specified.
nc_get_last(key, default_value=None)
Similar to nc_get()
however this function returns the last value in the list associated with the supplied key
. If the list is empty or none exists with that key, then the default_value
is returned.
let last_command = nc_get_last('executed_commands')
st.write(f'The last command executed was {last_command}')
nc_get_all()
This function returns the entire dictionary of values stored on the session state
let dictionary = nc_get_all()
nc_set(key, value)
This is the value executed when postMessage()
function is called and the NotificationCenter processes the contents successfully. The key
value is used as a dictionary key and the value
is appended to the array of previous messages for the same target.
nc_set('person', {'name': 'Brielle', 'gender': 'Female'})
nc_has(key)
This function returns True
if the supplied key exists in the dictionary of NotificationCenter values, or False
otherwise.
if nc_has('people'):
people = nc_get('people')
nc_clear(key)
This function will locate an existing key
in the dictionary. These keys have a list as their value. The .clear()
function will be invoked on the list.
nc_clear('people')
nc_clear_all()
This function removes all keys and values from the NotificationCenter data store.
nc_clear_all()
nc_add_substitution(str_or_re, val_or_fn)
This function allows you to specify a replacement or compiled regular expression that is used to search the contents of a to-be-rendered HTML snippet. The value or function (Callable) will be used in the place of the sought value. If the regular expression captured groups, they will be passed to the function as parameters.
nc_add_substitution(re.compile(r':exit-button:(.*?):'), lambda title: f'<button>{title}</button>')
nc_html(
f'''
<body>:exit-button:Click to exit:</body>
'''
)
This code would replace the ":exit-button:Click to exit:" with "Click to exit" as an example.
nc_add_script_src(src, attributes=None, raw=False)
This function takes a script source (URL or path) and optionally a dictionary of HTML attributes, and appends a formatted script tag or raw source to the session state list. If 'raw' is False, the 'src' and 'attributes' are used to create a ' calls.
nc_add_script_src('https://remotejslibrary.com/script.js')
nc_html(...)
nc_add_link_rel(src, attributes=None, raw=False)
This function takes a link source (URL or path) and optionally a dictionary of HTML attributes, and appends a formatted link tag or raw source to the '_nc_link_rels' session state list. If 'raw' is False, the 'href' and 'attributes' are used to create a '' tag using BeautifulSoup. Each of these tags are inserted in subsequent nc_html()
calls.
nc_add_link_rel('https://remotescript.com/style.css')
nc_html(...)
nc_add_script(script)
The function nc_html()
is a more function version of the st.components.v1.html()
. The nc_html()
function will pre-inject CSS, JavaScript and any HTML snippets previously setup as global pre-defines. The nc_add_script()
function allows you to add a snippet of JavaScript to all nc_html()
calls before the supplied HTML string.
nc_add_script(
f'''
Object.defineProperty(Array.prototype, 'first', { get: function() { return this?.[0] } })
'''
)
After running this function, the next time nc_html()
is invoked, the component added to the page will have access to JavaScript arrays with a getter on each instance called .first
that dynamically returns the first value or null
if there isn't one.
nc_add_style(style)
The function nc_html()
is a more function version of the st.components.v1.html()
. The nc_html()
function will pre-inject CSS, JavaScript and any HTML snippets previously setup as global pre-defines. The nc_add_style()
function allows you to add a snippet of CSS to all nc_html()
calls before the supplied HTML string.
nc_add_styles(f'* {{ font-size: 200%; }}')
When executed, all subsequent nc_html()
calls will find their contents' font sizes increased to 200%.
nc_add_html(html)
This function adds a new snippet of html that will be included in all calls to nc_html()
.
nc_add_html(f'<hr/>')
When next nc_html()
is executed, it will always be preceded by a horizontal rule.
nc_reset_script_srcs()
This function removes all previously specified script sources.
nc_reset_link_rels()
This function removes all previously specified link tags.
nc_reset_subs()
This function removes all previously specified substitutions.
nc_reset_scripts()
Resets all JavaScript snippets to the defaults. The defaults are the three aforementioned JavaScript Helper functions.
nc_reset_styles()
This function removes all globally added CSS snippets previously added using nc_add_style()
.
nc_reset_html()
Simlar in function to nc_reset_scripts()
or nc_reset_styles()
, the function nc_reset_html()
removes all previously added HTML snippets.
nc_listen(to, callback)
Probably one of the most useful function, this call will execute the supplied callback whenever any new value is set with a key
/target
that matches the to
value supplied to this function.
unsub = nc_listen('add_person', lambda person: print(person))
unsub()
This function also returns a function or Callable
that when invoked will trigger an unsubscribe on the internal event manager.
The creme of the crop, injects a component of HTML into the page. This HTML iframe that gets injected will be prefixed by any previous calls to nc_add_html()
, nc_add_style()
, and nc_add_script()
. Even when no other code is injected, at least the three helper JavaScript functions above will be present. This allows simple code like this to work:
nc_html(f"<a onclick=\"ncSendMessage('target', {{...}})\">Send Message</a>")
ns_use_bootstrap()
This function invokes both nc_add_link_rel()
and nc_add_script_src()
to include Bootstrap 5.13 CSS and JavaScript libraries for each subsequent use of nc_html()
.
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
from notification_center import notification_center, nc_html, nc_add_style, nc_listen
notification_center(key="globalnc")
nc_listen('data', lambda data: print('data added', data))
nc_add_script(
f'''
function sendData() {{ ncSendMessage('data', {{msg: 'Stylized Button Pressed'}}) }}
'''
)
nc_add_style(
f'''
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;
}}
'''
)
nc_html(f'<a class="stylized-button" onclick="sendData()">Button</a>')
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 is being used 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
st.session_state.setdefault('_nc_debug', False)
To turn it on, simply change the value to True
st.session_state._nc_debug = True
Visual examples
After this point you'll see NotificationCenter messages appearing in your Python console.