Custom Code Events and Style Selectors
Learn about how to write your own custom code using events and selectors.
Code snippets mentioned here should be added to the page Settings -> Custom Code -> Code Inside Header, unless stated otherwise.
Global variables
- There is a
<div>
element on top of the document<body>
that stores application id and page id on it’sdata-appid
anddata-pageid
attributes.
- If there is a logged in user
window.logged_in_user
object is available withsoftr_user_email
,softr_user_full_name
properties and in case the user is connected to Airtable it may have other fields coming from Airtable user record.
- Block specific data is stored on
window
object withhrid
as an identifier of that block. (ex.window['table1']
in case hrid is table1). Thewindow[hrid]
object differs based on block type.
- List, List details, Kanban, Chart, Organization chart, Calendar, Twitter and Map blocks have
baseId
andtableName
properties.
- Form block has
airtableBaseUrl
property
- Map block has
google_map_api_key
property
- There is
openSwModal
global method that opens given url in modal. Example:openSwModal('https://softr.io/')
Charts
Chart colors
To change chart default colors we should set
window['chart1-colors'] = ['#FE8070', '#DD7E6B', '#EA9999', '#F4CCCC', '#24A37D', '#AEAEB5', '#E25B5B', '#FFF974', '#4BAEAE', '#E5E5EA', '#33E4EF', '#C9DAF2'];
where chart1
is the block hrid.
Chart invalidate
Invalidate the chart cache after 5 seconds:
<script> const invalidateChartCache = new CustomEvent("invalidate-chart-cache-chart1"); window.dispatchEvent(invalidateChartCache); </script>
where chart1
is the block hrid.
Chart reload
Reload after 5 seconds:
<script> const refetchDataEvent = new CustomEvent("reload-chart1"); setTimeout(() => { window.dispatchEvent(refetchDataEvent); }, 5000); </script>
where chart1
is the block hrid.
Chart invalidate and reload
Invalidate the cache and reload the chart:
<script> const invalidateChartCache = new CustomEvent("invalidate-chart-cache-chart1"); const refetchDataEvent = new CustomEvent("reload-chart1"); setTimeout(() => { window.dispatchEvent(invalidateChartCache); window.dispatchEvent(refetchDataEvent); }, 5000); </script>
Where chart1
is the block hrid
Chart v5 change colors depending on value WIP
<script> window.addEventListener('chart-loaded-chart1', (event) => { const chartInstance = event.detail; let option = chartInstance.getOption(); const seriesData = option.series[0].data; for (let i = 0; i < seriesData.length; i++) { let dataItem = seriesData[i]; if (typeof dataItem === 'number') { dataItem = {value: dataItem}; } const value = dataItem.value; if (value > 100000) { dataItem.itemStyle = {color: 'red'}; } else { dataItem.itemStyle = {color: 'green'}; } seriesData[i] = dataItem; } option.series[0].data = seriesData; chartInstance.setOption(option) }) </script>
Browser Events
Adding event listeners to elements that were rendered by React is tricky. You may add an event listener but after React re-renders the component it might loose your listener because React can (in some cases) remove and re-create element that you added the listener on.
To handle this the event listener
should be added on parent element
that doesn’t re-render and check
the element selector
after event trigger
.
Example:
const handler = (e) => { if (e.target.closest('#table1 .ag-row')) { // handle click event on ag-row } }; document.body.addEventListener('click', handler);
This way you can handle click
event on table rows with table1
hrid.
Generic Custom Events
There are custom events that Softr blocks trigger or listen to. We also add hrid to the event name to identify the block the event refers to.
block-loaded
block-loaded
event is triggered when React mounts the block into DOM. It can be used instead of DOMContentLoaded
event that is used in old custom codes.
ex.
window.addEventListener('block-loaded-table1', () => { console.log('Block loaded'); });
get-record
get-record
event is triggered on every single data response from softr data service. It is used on list-details blocks and it can be used as for getting the data and using it for other 3rd party calls or as a indicator that after some small interval time the block will be fully rendered.
ex.
const onRecord = (e) => { // we got new data under e.details console.log(e.detail); //console.log {id: '***', fields: {...}} }; window.addEventListener('get-record-list-details1', onRecord);
const onRecord = (e) => { setTimeout(() => { // The block finish rendering // I may do some staff here. }, 50); }; window.addEventListener('get-record-list-details1', onRecord);
window.addEventListener('get-record-list-details1', (e) => { // hide list details block if no record found if (!e.detail.id) { document.getElementById('list-details1').classList.add('d-none'); } });
get-records
get-records
event is triggered on every data response from softr data service. It can be used as for getting the data and using it for other 3rd party calls or as a indicator that after some small interval time the block will be fully rendered.
ex.
const onRecords = (e) => { // we got new data under e.details console.log(e.detail); //console.log [{id: '***', fields: {...}}] }; window.addEventListener('get-records-table1', onRecords);
const onRecords = (e) => { setTimeout(() => { // The block finish rendering // I may do some staff here. }, 50); }; window.addEventListener('get-records-table1', onRecords);
Also there is an get-records:before
event that is triggered before sending request, It can be used to catch the inline filter or search field changes.
const onFilterChange = (e) => { console.log(e.detail); //console.log { search: '', filter: [ { field: 'size', value: 'M' } ] } }; window.addEventListener('get-records-table1:before', onFilterChange);
update-records
update-records
event is listened by all blocks that use external data.
It can be used to change/add/remove the data that should be rendered. Mostly it can be used in pair with get-records
.
ex.
const onRecords = (e) => { const modifiedRecords = e.detail.map(({fields, ...other}) => ({ ...other, fields: { ...fields, phone: fields.phone ? fields.phone.replace('+374', '0') : '', } })); const modify = new CustomEvent('update-records-table1', { detail: modifiedRecords }); setTimeout(() => window.dispatchEvent(modify), 1); }; window.addEventListener('get-records-table1', onRecords);
Action Buttons
add-record
add-record-hrid
event is fired when the form submission in the add-record modal is triggered.
add-record-success-hrid
event is fired when the submission is successful
add-record-failure-hrid
event is fired when the submission has failed
<script>
window.addEventListener("block-loaded-list1", () => {
window.addEventListener("add-record-list1", (e) => {
console.log("adding the record -> ", e);
});
window.addEventListener("add-record-success-list1", (e) => {
console.log("success -> ", e);
});
window.addEventListener("add-record-failure-list1", (e) => {
console.log("failure -> ", e);
});
});
</script>
update-record
update-record-success
event with field values is triggered after getting response with success status code.
update-record-failure
event is triggered after getting response with error code.
ex.
window.addEventListener('update-record-failure-list1', (e) => { console.log('update record failure', e.detail); // update record failure 'Email field is required.' }); // To reload the page after record update on #list1 block window.addEventListener('update-record-success-list1', () => { window.location.reload() });
upvote-record
upvote-record-success
event with field values is triggered after getting response with success status code and datail.
upvote-record-failure
event is triggered after getting response with error code.
ex.
<script> window.addEventListener('upvote-record-success-BLOCKNAME', (e) => { console.log('upvote-record detail ', e.detail) }); window.addEventListener('upvote-record-failure-BLOCKNAME', (e) => { console.log('upvote-record detail ', e) }); </script>
call-api (webhook-trigger)
call-api-success
event with field values is triggered after getting response with a success status code.
call-api-failure
event is triggered after getting response with error code.
ex.
<script> window.addEventListener('call-api-success-BLOCKNAME', (e) => { console.log('call-api-success', e.detail) }); window.addEventListener('call-api-failure-BLOCKNAME', (e) => { console.log('call-api-failure', e) }); </script>
Blocks Custom events
Calendar block trigger reload block
ex.
<script> window.addEventListener('block-loaded-calendar1', () => { const customEvent = new CustomEvent('reload-block-calendar1'); window.dispatchEvent(customEvent); }); </script>
List blocks trigger reload block
ex.
<script> window.addEventListener('update-record-success-list-details1', () => { //IF THE DETAILS BLOCK AND LIST BLOCK ON THE SAME PAGE window.dispatchEvent(new CustomEvent('reload-block-BLOCKNAME')); //IF THE DETAILS PAGE OPENED IN MODAL window.parent.dispatchEvent(new CustomEvent('reload-block-BLOCKNAME')); //RELOAD PAGE window.location.reload() }); </script>
Form Custom Events
update-fields
update-fields
event is listened by blocks that use form inputs with Formik library. Currently it’s blocks under Form and User Accounts categories. It can be used to update input values with custom code. This is used to update form values from a code automatically… let’s say after form is rendered client side custom code fetches a data from third party API and wants to set into form to be submitted… (keep in mind user attributes and URL attributes can be prefilled with default functionality without code)
ex.
<script> window.addEventListener('block-loaded-form1', () => { const updateFields = new CustomEvent('update-fields-form1', { detail: { 'Full Name': 'Softr', 'Email': 'info@softr.io' } }); window.dispatchEvent(updateFields); }); </script>
submit-form, submit-form-success, submit-form-failure
submit-form
event with field values as an attribute is triggered before sending form submission request.
submit-form-success
event with field values is triggered after getting response with success status code.
submit-form-failure
event is triggered after getting response with error code.
ex.
window.addEventListener('submit-form-form1', (e) => { // e.detail is an object with form field names and values console.log('form submit', e.detail); // form submit { "Full Name": "Softr", "Email": "info@softr.io" } }); window.addEventListener('submit-form-success-form1', (e) => { // e.detail is an object with form field names and values console.log('form submit success', e.detail); // form submit success { // payload: { "Full Name": "Softr" ... }, // response: { headers: {...}, data: {...}, status: 200 } // } }); window.addEventListener('submit-form-failure-form1', (e) => { console.log('form submit failure', e.detail); // form submit failure 'Email field is required.' });
set-form-dropdown-values
set-form-dropdown-values
can be used to set dropdown values dynamically.
ex.
window.addEventListener('block-loaded-form1', () => { const updateOptions = new CustomEvent('set-form-dropdown-values-form1', { detail: { size: ['S', 'M', 'L'], color: ['Red', 'Blue', 'Yellow'] } }); window.dispatchEvent(updateOptions); });
Customize form’s validation messages
Make error massages of inputs in forms and user account blocks changeable/translatable via a custom code:
<script>
window["softr_validation_messages"] = {
required: "the required message",
email: "email field's custom message",
phone: "phone field's custom message",
url: "url field's custom message"
};
</script>
Header Custom Events
set-logo-link
set-logo-link
event is used to set header logo link to custom url.
ex.
window.addEventListener('block-loaded-header1', () => { const detail = { link: 'https://google.com' }; window.dispatchEvent(new CustomEvent('set-logo-link-header1', { detail })); });
trigger action before logout
first will trigger custom action and after 300 ms will continue to Sign Out
ex.
window.addEventListener('user-sign-out', (e) => { // do some actions before logout - (300 ms) });
Sign-out user by clicking on custom button
block-loaded
event is triggered when React mounts the block into DOM.
ex.
<script> window.addEventListener('block-loaded-BLOCKNAME', () => { setTimeout(() => { const signOutButton = document.querySelector('BUTTON_CLASS_NAME'); signOutButton.addEventListener('click', () => { document.cookie = 'jwtToken=;path=/;expires=Thu, 01 Jan 1970 00:00:00 UTC;SameSite=None;Secure'; window.location.href = '/'; }); }, 1000) }); </script>
Styling
There is still bootstrap included in the page, but it will be removed in the near future. So try not to use it.
Material-ui React component library is used in the new blocks. It adds classes to all small to large components with the prefix of Mui
those classes can be used to add custom styles to the elements, but we may also change it to Softr
or something similar in the near future (ex. MuiInputBase
to SoftrInputBase
).
Try wrapping selector with hrid to add more priority to your selector. ex.
#table1 .MuiInputBase-input { border-left: none; }
There are also some attributes that expose the content of the element. It can help identify and style them based on it.
- data-content attribute on tag elements
- data-rating attribute on rating elements
Because CSS supports attribute selectors, they can be used to style the elements based on content.
ex.
// tags with content "Low" #table1 .tag-item[data-content="Low"] { background-color: #F00 !important; } // add span to the selector if you want to customize text color #table1 .tag-item[data-content="Low"] span { color: #FFF !important; } // tags that start with word "Low" #table1 .tag-item[data-content^="Low"] { background-color: #F00 !important; } // tags that have substring "low" #table1 .tag-item[data-content*="low"] { background-color: #F00 !important; } // ratings that are 1 start #table1 [data-rating="1"] span { color: #F00 !important; }
Kanban block custom column background colors
ex.
<script> // block-loaded-BLOCKNAME --> example (block-loaded-kanban1) window.addEventListener('block-loaded-kanban1', () => { // colorMap object values should be the same as column label names const colorMap = { '1': { 'background': '#1E8700' }, '2': { 'background': '#F6CF2D' }, '3': { 'background': '#EC1212' }, }; const lookingForColumns = setInterval(() => { const columns = [...document.querySelectorAll(`[data-column-name]`)]; if(columns.length){ clearInterval(lookingForColumns); columns.forEach(col => { const columnName= $(col).attr('data-column-name'); if(colorMap[columnName]){ $(col).css('background', colorMap[columnName].background); } }); } }, 300); }); </script>
Styles and actions for Action buttons by custom code
ex.
// ✅ get an element with data-action-button-id const el1 = document.querySelector('[data-action-button-id="list1-visible-btn-0-rec1yIahkX1mZhLQn"]'); // ✅ get an element where data-action-button-id starts with list1-visible-btn or list1-btn const el1 = document.querySelector('[data-action-button-id^="list1-visible-btn-"]'); // ✅ get all elements where data-action-button-id starts with list1-visible-btn const elements = document.querySelectorAll('[data-action-button-id^="list1-visible-btn-"]'); #list2 button[data-action-button-id^="list2-visible-btn-_l26zaezue"] { margin-left: 50px }
// ✅ style for all buttons where data-action-button-id starts with list1-visible-btn- #list1 button[data-action-button-id^="list1-visible-btn-"] { margin-left: 50px } // ✅ style for one of buttons with data-action-button-id-BUTTONID #list1 button[data-action-button-id^="list1-visible-btn-_l26zaezue"] { margin-left: 50px }
Last updated on August 4, 2021