{
private renderBlock(
element: BlockElement,
index: number,
- width: number
+ width: number,
+ deltaBlock: IBlock
): ReactNode {
+ const BlockType = deltaBlock.collapsible
+ ? this.WithCollapsibleBlock
+ : Block
+ const optionalProps = deltaBlock.collapsible ? deltaBlock.collapsible : {}
return (
- {
uploadClient={this.props.uploadClient}
widgetsDisabled={this.props.widgetsDisabled}
componentRegistry={this.props.componentRegistry}
+ deltaBlock={deltaBlock}
+ {...optionalProps}
/>
)
diff --git a/frontend/src/hocs/withCollapsible/index.tsx b/frontend/src/hocs/withCollapsible/index.tsx
new file mode 100644
index 000000000000..e7370d220d79
--- /dev/null
+++ b/frontend/src/hocs/withCollapsible/index.tsx
@@ -0,0 +1,18 @@
+/**
+ * @license
+ * Copyright 2018-2020 Streamlit Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export { default } from "./withCollapsible"
diff --git a/frontend/src/hocs/withCollapsible/withCollapsible.test.tsx b/frontend/src/hocs/withCollapsible/withCollapsible.test.tsx
new file mode 100644
index 000000000000..647de37700bd
--- /dev/null
+++ b/frontend/src/hocs/withCollapsible/withCollapsible.test.tsx
@@ -0,0 +1,62 @@
+/**
+ * @license
+ * Copyright 2018-2020 Streamlit Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React, { ComponentType } from "react"
+import { shallow } from "enzyme"
+import withCollapsible, { Props, StyledToggle } from "./withCollapsible"
+
+const testComponent: ComponentType = () => test
+
+const getProps = (props: Partial = {}): Props => ({
+ label: "hi",
+ collapsible: true,
+ ...props,
+})
+
+describe("withCollapsible HOC", () => {
+ it("renders without crashing", () => {
+ const props = getProps()
+ const WithHoc = withCollapsible(testComponent)
+ // @ts-ignore
+ const wrapper = shallow()
+
+ expect(wrapper.html()).not.toBeNull()
+ })
+
+ it("should render a expanded component", () => {
+ const props = getProps()
+ const WithHoc = withCollapsible(testComponent)
+ // @ts-ignore
+ const wrapper = shallow()
+ const toggleHeader = wrapper.find(StyledToggle)
+
+ expect(toggleHeader.exists()).toBeTruthy()
+ expect(toggleHeader.text()).toEqual("Hide")
+ })
+
+ it("should render a collapsed component", () => {
+ const props = getProps({
+ collapsed: true,
+ })
+ const WithHoc = withCollapsible(testComponent)
+ // @ts-ignore
+ const wrapper = shallow()
+ const toggleHeader = wrapper.find(StyledToggle)
+
+ expect(toggleHeader.text()).toEqual("Show")
+ })
+})
diff --git a/frontend/src/hocs/withCollapsible/withCollapsible.tsx b/frontend/src/hocs/withCollapsible/withCollapsible.tsx
new file mode 100644
index 000000000000..4a0b4840fd5b
--- /dev/null
+++ b/frontend/src/hocs/withCollapsible/withCollapsible.tsx
@@ -0,0 +1,92 @@
+/**
+ * @license
+ * Copyright 2018-2020 Streamlit Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React, { ComponentType, ReactElement, useEffect, useState } from "react"
+import { styled } from "styletron-react"
+import { colors, variables } from "lib/widgetTheme"
+
+export interface Props {
+ collapsible: boolean
+ label: string
+ collapsed: boolean
+}
+
+type ComponentProps = {
+ collapsed: boolean
+}
+
+export const AnimatedComponentWrapper = styled(
+ "div",
+ ({ collapsed }: ComponentProps) => ({
+ maxHeight: collapsed ? 0 : "100vh",
+ overflow: "hidden",
+ transitionProperty: "max-height",
+ transitionDuration: "0.5s",
+ transitionTimingFunction: "ease-in-out",
+ })
+)
+
+export const StyledHeader = styled("div", ({ collapsed }: ComponentProps) => ({
+ display: "flex",
+ justifyContent: "space-between",
+ cursor: "pointer",
+ borderWidth: 0,
+ borderBottomWidth: collapsed ? 0 : "1px",
+ borderStyle: "solid",
+ borderColor: colors.grayLighter,
+ marginBottom: variables.spacer,
+ transitionProperty: "border-bottom-width",
+ transitionDuration: "0.5s",
+ transitionTimingFunction: "ease-in-out",
+}))
+
+export const StyledToggle = styled("small", {
+ color: colors.gray,
+})
+
+function withCollapsible(
+ WrappedComponent: ComponentType
+): ComponentType {
+ const CollapsibleComponent = (props: Props): ReactElement => {
+ const { label, collapsed: initialCollapsed, ...componentProps } = props
+
+ const [collapsed, toggleCollapse] = useState(initialCollapsed)
+ useEffect(() => {
+ toggleCollapse(initialCollapsed)
+ }, [initialCollapsed])
+
+ const toggle = (): void => toggleCollapse(!collapsed)
+
+ return (
+ <>
+
+ {label}
+
+ {collapsed ? "Show" : "Hide"}
+
+
+
+
+
+ >
+ )
+ }
+
+ return CollapsibleComponent
+}
+
+export default withCollapsible
diff --git a/frontend/src/lib/DeltaParser.ts b/frontend/src/lib/DeltaParser.ts
index 24fd39145e7d..3dfa63838be2 100644
--- a/frontend/src/lib/DeltaParser.ts
+++ b/frontend/src/lib/DeltaParser.ts
@@ -83,11 +83,11 @@ export function applyDelta(
handleNewElementMessage(currentElement, element, reportId, metadata)
)
},
- newBlock: () => {
+ addBlock: (deltaBlock: Delta) => {
elements[topLevelBlock] = elements[topLevelBlock].updateIn(
deltaPath,
reportElement =>
- handleNewBlockMessage(reportElement, reportId, metadata)
+ handleAddBlockMessage(reportElement, reportId, metadata, deltaBlock)
)
},
addRows: (namedDataSet: NamedDataSet) => {
@@ -129,25 +129,21 @@ function handleNewElementMessage(
})
}
-function handleNewBlockMessage(
+function handleAddBlockMessage(
reportElement: ReportElement,
reportId: string,
- metadata: IForwardMsgMetadata
+ metadata: IForwardMsgMetadata,
+ deltaBlock: Delta
): ReportElement {
MetricsManager.current.incrementDeltaCounter("new block")
- // There's nothing at this node (aka first run), so initialize an empty list.
- if (!reportElement) {
- return ImmutableMap({ element: List(), reportId, metadata })
- }
-
- // This node was already a list of elements; no need to change anything.
- if (reportElement.get("element") instanceof List) {
- return reportElement
- }
+ // This node was already a list of elements. Update everything but the element.
+ const list =
+ reportElement && reportElement.get("element") instanceof List
+ ? reportElement.get("element")
+ : List()
- // This node used to represent a single element; convert into an empty list.
- return reportElement.set("element", List())
+ return ImmutableMap({ element: list, reportId, metadata, deltaBlock })
}
function handleAddRowsMessage(
diff --git a/frontend/src/lib/widgetTheme.ts b/frontend/src/lib/widgetTheme.ts
index bd80d0e9fdde..9c7799b08788 100644
--- a/frontend/src/lib/widgetTheme.ts
+++ b/frontend/src/lib/widgetTheme.ts
@@ -34,6 +34,12 @@ const lineHeightTight = SCSS_VARS["$line-height-tight"]
const smallTextMargin = SCSS_VARS["$m2-3-font-size-sm"]
const textMargin = SCSS_VARS["$font-size-sm"]
const tinyTextMargin = SCSS_VARS["$m1-2-font-size-sm"]
+const spacer = SCSS_VARS.$spacer
+
+export const variables = {
+ borderRadius,
+ spacer,
+}
// Colors
export const colors = {
diff --git a/lib/streamlit/__init__.py b/lib/streamlit/__init__.py
index db7ebea7dcf1..485aa04f3616 100644
--- a/lib/streamlit/__init__.py
+++ b/lib/streamlit/__init__.py
@@ -181,6 +181,7 @@ def _update_logger():
write = _main.write # noqa: E221
beta_color_picker = _main.beta_color_picker # noqa: E221
container = _main.container # noqa: E221
+collapsible_container = _main.collapsible_container # noqa: E221
# Config
diff --git a/lib/streamlit/delta_generator.py b/lib/streamlit/delta_generator.py
index ce5f9925ebf4..92fb26224ecc 100644
--- a/lib/streamlit/delta_generator.py
+++ b/lib/streamlit/delta_generator.py
@@ -22,6 +22,7 @@
from streamlit.report_thread import get_report_ctx
from streamlit.errors import StreamlitAPIException, StreamlitDeprecationWarning
from streamlit.errors import NoSessionContext
+from streamlit.proto import Block_pb2
from streamlit.proto import BlockPath_pb2
from streamlit.proto import ForwardMsg_pb2
from streamlit.proto.Element_pb2 import Element
@@ -352,7 +353,7 @@ def _enqueue(
return _value_or_dg(return_value, output_dg)
- def container(self):
+ def _block(self, block_proto=Block_pb2.Block()):
# Switch to the active DeltaGenerator, in case we're in a `with` block.
self = self._active_dg
@@ -360,10 +361,10 @@ def container(self):
return self
msg = ForwardMsg_pb2.ForwardMsg()
- msg.delta.new_block = True
msg.metadata.parent_block.container = self._container
msg.metadata.parent_block.path[:] = self._cursor.path
msg.metadata.delta_id = self._cursor.index
+ msg.delta.add_block.CopyFrom(block_proto)
# Normally we'd return a new DeltaGenerator that uses the locked cursor
# below. But in this case we want to return a DeltaGenerator that uses
@@ -377,13 +378,62 @@ def container(self):
# Must be called to increment this cursor's index.
self._cursor.get_locked_cursor(last_index=None)
-
_enqueue_message(msg)
return block_dg
+ def container(self):
+ return self._block()
+
+ def collapsible_container(self, label=None, collapsed=False):
+ """Creates a collapsible container.
+
+ [TODO: get more container verbage]
+ Similar to `st.container`, `st.collapsible_container` provides a container
+ to add elements to. However, it has the added benefit of being collapsible.
+ Users will be able to expand and collapse the container that is identifiable
+ with the provided label.
+
+ Parameters
+ ----------
+ label : str
+ A short label used as the header for the collapsible container.
+ This will always be displayed even when the container is collapsed.
+ collapsed : boolean
+ The default state for the collapsible container.
+ Defaults to False
+
+ Returns
+ -------
+ [TODO] Technically a delta generator but let's please not tell the users that...
+
+ Examples
+ --------
+ >>> collapsible_container = st.collapsible_container("Collapse Me")
+ >>> collapsible_container.write("I can be collapsed")
+
+ """
+ if label is None:
+ raise StreamlitAPIException(
+ "A label is required for a collapsible container"
+ )
+
+ collapsible_proto = Block_pb2.Block.Collapsible()
+ collapsible_proto.collapsed = collapsed
+ collapsible_proto.label = label
+
+ block_proto = Block_pb2.Block()
+ block_proto.collapsible.CopyFrom(collapsible_proto)
+
+ return self._block(block_proto=block_proto)
+
def favicon(
- self, element, image, clamp=False, channels="RGB", format="JPEG",
+ self,
+ element,
+ image,
+ clamp=False,
+ channels="RGB",
+ format="JPEG",
):
"""Set the page favicon to the specified image.
diff --git a/lib/streamlit/report_queue.py b/lib/streamlit/report_queue.py
index 2cc7b6398fe7..893bf1ad3563 100644
--- a/lib/streamlit/report_queue.py
+++ b/lib/streamlit/report_queue.py
@@ -133,7 +133,7 @@ def compose_deltas(old_delta, new_delta):
if new_delta_type == "new_element":
return new_delta
- elif new_delta_type == "new_block":
+ elif new_delta_type == "add_block":
return new_delta
elif new_delta_type == "add_rows":
diff --git a/proto/streamlit/proto/Block.proto b/proto/streamlit/proto/Block.proto
new file mode 100644
index 000000000000..fa5857314c1a
--- /dev/null
+++ b/proto/streamlit/proto/Block.proto
@@ -0,0 +1,26 @@
+/**
+ * Copyright 2018-2020 Streamlit Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+*/
+
+syntax = "proto3";
+
+message Block {
+ Collapsible collapsible = 1;
+
+ message Collapsible {
+ string label = 1;
+ bool collapsed = 2;
+ }
+}
diff --git a/proto/streamlit/proto/Delta.proto b/proto/streamlit/proto/Delta.proto
index 148145d496da..49478ea03e57 100644
--- a/proto/streamlit/proto/Delta.proto
+++ b/proto/streamlit/proto/Delta.proto
@@ -16,6 +16,7 @@
syntax = "proto3";
+import "streamlit/proto/Block.proto";
import "streamlit/proto/Element.proto";
import "streamlit/proto/NamedDataSet.proto";
@@ -26,7 +27,7 @@ message Delta {
Element new_element = 3;
// Append a new block to the report.
- bool new_block = 4;
+ Block add_block = 6;
// Append data to a DataFrame in for current element. The element to add to
// is identified by the ID field, above. The dataframe is identified either