Browse Source

Merge branch 'master' into 98-builtin-slot

pull/109/head
Michael Shanks 6 years ago
committed by GitHub
parent
commit
83953922d7
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 17
      packages/builder/src/builderStore/insertCodeMetadata.js
  2. 40
      packages/builder/src/builderStore/store.js
  3. 55
      packages/builder/src/common/ConfirmDialog.svelte
  4. 23
      packages/builder/src/common/Icons/XCircle.svelte
  5. 1
      packages/builder/src/common/Icons/index.js
  6. 22
      packages/builder/src/userInterface/ComponentsHierarchy.svelte
  7. 45
      packages/builder/src/userInterface/ComponentsHierarchyChildren.svelte
  8. 8
      packages/builder/src/userInterface/EventsEditor/EventEditorModal.svelte
  9. 14
      packages/builder/src/userInterface/EventsEditor/EventsEditor.svelte
  10. 20
      packages/builder/src/userInterface/UserInterfaceRoot.svelte
  11. 1
      packages/client/package.json
  12. 110
      packages/client/src/createApp.js
  13. 4
      packages/client/src/index.js
  14. 70
      packages/client/src/render/attachChildren.js
  15. 53
      packages/client/src/render/prepareRenderComponent.js
  16. 70
      packages/client/src/state/bbComponentApi.js
  17. 3
      packages/client/src/state/stateBinding.js
  18. 288
      packages/client/src/state/stateManager.js
  19. 174
      packages/client/tests/bindingDom.spec.js
  20. 58
      packages/client/tests/testAppDef.js
  21. 185
      packages/materialdesign-components/components.json
  22. 1
      packages/materialdesign-components/package.json
  23. 12
      packages/materialdesign-components/src/Button.svelte
  24. 4
      packages/materialdesign-components/src/Button/Button.svelte
  25. 2
      packages/materialdesign-components/src/Button/index.js
  26. 4
      packages/materialdesign-components/src/Checkbox/index.js
  27. 0
      packages/materialdesign-components/src/Common/Icon.svelte
  28. 0
      packages/materialdesign-components/src/Common/Ripple.js
  29. 67
      packages/materialdesign-components/src/Datatable/Datatable.svelte
  30. 23
      packages/materialdesign-components/src/Datatable/DatatableCell.svelte
  31. 26
      packages/materialdesign-components/src/Datatable/DatatableRow.svelte
  32. 1
      packages/materialdesign-components/src/Datatable/_style.scss
  33. 2
      packages/materialdesign-components/src/Datatable/index.js
  34. 8
      packages/materialdesign-components/src/H1.svelte
  35. 4
      packages/materialdesign-components/src/Radiobutton/Radiobutton.svelte
  36. 4
      packages/materialdesign-components/src/Radiobutton/index.js
  37. 34
      packages/materialdesign-components/src/Test/TestApp.svelte
  38. 38
      packages/materialdesign-components/src/Test/props.js
  39. 30
      packages/materialdesign-components/src/Test/testComponents.js
  40. 2
      packages/materialdesign-components/src/Textfield/Textfield.svelte
  41. 2
      packages/materialdesign-components/src/Textfield/index.js
  42. 8
      packages/materialdesign-components/src/Typography/index.js
  43. 12
      packages/materialdesign-components/src/index.js
  44. 92
      packages/materialdesign-components/src/scripts/publishDev.js
  45. 3
      packages/server/utilities/builder/buildPage.js
  46. 9
      packages/server/utilities/builder/deleteCodeMeta.js
  47. 6
      packages/server/utilities/builder/index.js
  48. 4
      packages/standard-components/src/button.svelte

17
packages/builder/src/builderStore/insertCodeMetadata.js

@ -0,0 +1,17 @@
export const insertCodeMetadata = props => {
if (props._code && props._code.length > 0) {
props._codeMeta = codeMetaData(props._code)
}
if (!props._children || props._children.length === 0) return
for (let child of props._children) {
insertCodeMetadata(child)
}
}
const codeMetaData = code => {
return {
dependsOnStore: RegExp(/(store.)/g).test(code),
}
}

40
packages/builder/src/builderStore/store.js

@ -40,6 +40,7 @@ import {
} from "./loadComponentLibraries"
import { buildCodeForScreens } from "./buildCodeForScreens"
import { generate_screen_css } from "./generate_css"
import { insertCodeMetadata } from "./insertCodeMetadata"
// import { uuid } from "./uuid"
let appname = ""
@ -115,6 +116,7 @@ export const getStore = () => {
store.setComponentStyle = setComponentStyle(store)
store.setComponentCode = setComponentCode(store)
store.setScreenType = setScreenType(store)
store.deleteComponent = deleteComponent(store)
return store
}
@ -824,6 +826,8 @@ const setCurrentScreenFunctions = s => {
s.currentPreviewItem === "screen"
? buildCodeForScreens([s.currentPreviewItem])
: "({});"
insertCodeMetadata(s.currentPreviewItem.props)
}
const setScreenType = store => type => {
@ -840,3 +844,39 @@ const setScreenType = store => type => {
return s
})
}
const deleteComponent = store => component => {
store.update(s => {
let parent
walkProps(s.currentPreviewItem.props, (p, breakWalk) => {
if (p._children.includes(component)) {
parent = p
breakWalk()
}
})
if (parent) {
parent._children = parent._children.filter(c => c !== component)
}
s.currentFrontEndType === "page"
? _savePage(s)
: _saveScreenApi(s.currentPreviewItem, s)
return s
})
}
const walkProps = (props, action, cancelToken = null) => {
cancelToken = cancelToken || { cancelled: false }
action(props, () => {
cancelToken.cancelled = true
})
if (props._children) {
for (let child of props._children) {
if (cancelToken.cancelled) return
walkProps(child, action, cancelToken)
}
}
}

55
packages/builder/src/common/ConfirmDialog.svelte

@ -0,0 +1,55 @@
<script>
import Button from "./Button.svelte"
import ButtonGroup from "./ButtonGroup.svelte"
import UIkit from "uikit"
export let title=""
export let body=""
export let okText = "OK"
export let cancelText = "Cancel"
export let onOk = ()=> {}
export let onCancel = ()=> {}
export const show = () => {
UIkit.modal(theModal).show()
}
export const hide = () => {
UIkit.modal(theModal).hide()
}
let theModal;
const cancel = () => {
hide()
onCancel()
}
const ok = () => {
hide()
onOk()
}
</script>
<div id="my-id" uk-modal bind:this={theModal}>
<div class="uk-modal-dialog">
<button class="uk-modal-close-default" type="button" uk-close></button>
<div class="uk-modal-header">
<h2 class="uk-modal-title">{title}</h2>
</div>
<div class="uk-modal-body">{body}</div>
<div class="uk-modal-footer">
<ButtonGroup>
<Button grouped color="primary" on:click={ok}>
{okText}
</Button>
<Button grouped color="secondary" on:click={cancel}>
{cancelText}
</Button>
</ButtonGroup>
</div>
</div>
</div>

23
packages/builder/src/common/Icons/XCircle.svelte

@ -0,0 +1,23 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="feather feather-x-circle">
<circle cx="12" cy="12" r="10"/>
<line x1="15" y1="9" x2="9" y2="15"/>
<line x1="9" y1="9" x2="15" y2="15"/>
</svg>
<style>
svg {
height: 100%;
width: 100%;
}
</style>

After

Width:  |  Height:  |  Size: 454 B

1
packages/builder/src/common/Icons/index.js

@ -7,3 +7,4 @@ export { default as ArrowDownIcon } from "./ArrowDown.svelte"
export { default as CircleIndicator } from "./CircleIndicator.svelte"
export { default as PencilIcon } from "./Pencil.svelte"
export { default as EventsIcon } from "./Events.svelte"
export { default as XCircleIcon } from "./XCircle.svelte"

22
packages/builder/src/userInterface/ComponentsHierarchy.svelte

@ -2,13 +2,16 @@
import ComponentsHierarchyChildren from "./ComponentsHierarchyChildren.svelte"
import { last, sortBy, map, trimCharsStart, trimChars, join } from "lodash/fp"
import ConfirmDialog from "../common/ConfirmDialog.svelte"
import { pipe } from "../common/core"
import { store } from "../builderStore"
import { ArrowDownIcon } from "../common/Icons/"
export let screens = []
let confirmDeleteDialog
let componentToDelete = ""
const joinPath = join("/")
const normalizedName = name =>
@ -23,6 +26,7 @@
)
const lastPartOfName = c =>
c &&
last(c.name ? c.name.split("/") : c._component.split("/"))
const isComponentSelected = (current, comp) => current === comp
@ -38,6 +42,12 @@
component.component &&
$store.currentPreviewItem &&
component.component.name === $store.currentPreviewItem.name
const confirmDeleteComponent = component => {
componentToDelete = component
confirmDeleteDialog.show()
}
</script>
<div class="root">
@ -63,12 +73,20 @@
<ComponentsHierarchyChildren
components={screen.component.props._children}
currentComponent={$store.currentComponentInfo}
onSelect={store.selectComponent} />
onSelect={store.selectComponent}
onDeleteComponent={confirmDeleteComponent}/>
{/if}
{/each}
</div>
<ConfirmDialog
bind:this={confirmDeleteDialog}
title="Confirm Delete"
body={`Are you sure you wish to delete this '${lastPartOfName(componentToDelete._component)}' component`}
okText="Delete Component"
onOk={() => store.deleteComponent(componentToDelete)}/>
<style>
.root {
font-weight: 500;

45
packages/builder/src/userInterface/ComponentsHierarchyChildren.svelte

@ -1,20 +1,24 @@
<script>
import { last } from "lodash/fp"
import { pipe } from "../common/core"
import { XCircleIcon } from "../common/Icons"
export let components = []
export let currentComponent
export let onSelect = () => {}
export let level = 0
export let onDeleteComponent
const capitalise = s => s.substring(0, 1).toUpperCase() + s.substring(1)
const get_name = s => last(s.split("/"))
const get_name = s => !s ? "" : last(s.split("/"))
const get_capitalised_name = name =>
pipe(
name,
[get_name, capitalise]
)
</script>
<ul>
@ -24,7 +28,12 @@
class="item"
class:selected={currentComponent === component}
style="padding-left: {level * 20 + 67}px">
{get_capitalised_name(component._component)}
<span class="item-name">{get_capitalised_name(component._component)}</span>
<button
class="delete-component"
on:click={() => onDeleteComponent(component)}>
<XCircleIcon />
</button>
</span>
{#if component._children}
@ -32,7 +41,8 @@
components={component._children}
{currentComponent}
{onSelect}
level={level + 1} />
level={level + 1}
{onDeleteComponent} />
{/if}
</li>
{/each}
@ -45,14 +55,39 @@
margin: 0;
}
.item {
display: block;
padding: 11px 67px;
display: flex;
flex-direction: row;
padding: 11px 5px 11px 67px;
border-radius: 3px;
}
.item > span {
width: 1px;
flex: 1 1 auto;
}
.item > button {
display: none;
height: 20px;
color: var(--slate)
}
.item:hover {
background: #fafafa;
cursor: pointer;
}
.item:hover > button {
border-style: none;
background: rgba(0,0,0,0);
display: block;
cursor: pointer;
}
.item:hover > button:hover {
color: var(--button-text);
}
.selected {
color: var(--button-text);
background: var(--background-button) !important;

8
packages/builder/src/userInterface/EventsEditor/EventEditorModal.svelte

@ -11,15 +11,17 @@
import { EVENT_TYPE_MEMBER_NAME } from "../../common/eventHandlers"
export let event
export let eventOptions
export let eventOptions = []
export let open
export let onClose
export let onPropChanged
let eventType = "onClick"
let eventType = ""
let draftEventHandler = { parameters: [] }
$: eventData = event || { handlers: [] }
$: if (!eventOptions.includes(eventType) && eventOptions.length > 0)
eventType = eventOptions[0].name
const closeModal = () => {
onClose()
@ -74,7 +76,7 @@
<h5>Event Type</h5>
{@html getIcon('info', 20)}
</header>
<Select :value={eventType}>
<Select bind:value={eventType}>
{#each eventOptions as option}
<option value={option.name}>{option.name}</option>
{/each}

14
packages/builder/src/userInterface/EventsEditor/EventsEditor.svelte

@ -34,16 +34,10 @@
let selectedEvent = null
$: {
events = Object.keys(component)
.filter(key => findType(key) === EVENT_TYPE)
.map(key => ({ name: key, handlers: component[key] }))
}
function findType(propName) {
if (!component._component) return
return components.find(({ name }) => name === component._component).props[
propName
]
const componentDefinition = components.find(c => c.name === component._component)
events = Object.keys(componentDefinition.props)
.filter(propName => componentDefinition.props[propName].type === EVENT_TYPE)
.map(propName => ({ name: propName, handlers: (component[propName] || []) }))
}
const openModal = event => {

20
packages/builder/src/userInterface/UserInterfaceRoot.svelte

@ -10,8 +10,12 @@
import SettingsView from "./SettingsView.svelte"
import PageView from "./PageView.svelte"
import ComponentsPaneSwitcher from "./ComponentsPaneSwitcher.svelte"
import ConfirmDialog from "../common/ConfirmDialog.svelte"
import { last } from "lodash/fp"
let newComponentPicker
let confirmDeleteDialog
let componentToDelete = ""
const newComponent = () => {
newComponentPicker.show()
@ -21,6 +25,14 @@
const settings = () => {
settingsView.show()
}
const confirmDeleteComponent = component => {
componentToDelete = component
confirmDeleteDialog.show()
}
const lastPartOfName = c => c ? c.split("/") : ""
</script>
<div class="root">
@ -53,6 +65,7 @@
<ComponentsHierarchyChildren
components={$store.currentPreviewItem.props._children}
currentComponent={$store.currentComponentInfo}
onDeleteComponent={confirmDeleteComponent}
onSelect={store.selectComponent}
level={-2} />
{/if}
@ -101,6 +114,13 @@
<NewComponent bind:this={newComponentPicker} />
<SettingsView bind:this={settingsView} />
<ConfirmDialog
bind:this={confirmDeleteDialog}
title="Confirm Delete"
body={`Are you sure you wish to delete this '${lastPartOfName(componentToDelete._component)}' component`}
okText="Delete Component"
onOk={() => store.deleteComponent(componentToDelete)}/>
<style>
button {
cursor: pointer;

1
packages/client/package.json

@ -36,6 +36,7 @@
"dependencies": {
"@nx-js/compiler-util": "^2.0.0",
"bcryptjs": "^2.4.3",
"deep-equal": "^2.0.1",
"lodash": "^4.17.15",
"lunr": "^2.3.5",
"regexparam": "^1.3.0",

110
packages/client/src/createApp.js

@ -1,74 +1,46 @@
import { writable } from "svelte/store"
import { createCoreApi } from "./core"
import { getStateOrValue } from "./state/getState"
import { setState, setStateFromBinding } from "./state/setState"
import { trimSlash } from "./common/trimSlash"
import { isBound } from "./state/isState"
import { attachChildren } from "./render/attachChildren"
import { createTreeNode } from "./render/renderComponent"
import { createTreeNode } from "./render/prepareRenderComponent"
import { screenRouter } from "./render/screenRouter"
import { createStateManager } from "./state/stateManager"
export const createApp = (
document,
componentLibraries,
frontendDefinition,
backendDefinition,
user,
uiFunctions
uiFunctions,
window
) => {
const coreApi = createCoreApi(backendDefinition, user)
backendDefinition.hierarchy = coreApi.templateApi.constructHierarchy(
backendDefinition.hierarchy
)
const pageStore = writable({
_bbuser: user,
})
const relativeUrl = url =>
frontendDefinition.appRootPath
? frontendDefinition.appRootPath + "/" + trimSlash(url)
: url
const apiCall = method => (url, body) =>
fetch(relativeUrl(url), {
method: method,
headers: {
"Content-Type": "application/json",
},
body: body && JSON.stringify(body),
})
const api = {
post: apiCall("POST"),
get: apiCall("GET"),
patch: apiCall("PATCH"),
delete: apiCall("DELETE"),
}
const safeCallEvent = (event, context) => {
const isFunction = obj =>
!!(obj && obj.constructor && obj.call && obj.apply)
if (isFunction(event)) event(context)
}
let routeTo
let currentScreenStore
let currentScreenUbsubscribe
let currentUrl
let screenStateManager
const onScreenSlotRendered = screenSlotNode => {
const onScreenSelected = (screen, store, url) => {
const { getInitialiseParams, unsubscribe } = attachChildrenParams(store)
const stateManager = createStateManager({
store,
coreApi,
frontendDefinition,
componentLibraries,
uiFunctions,
onScreenSlotRendered: () => {},
})
const getAttchChildrenParams = attachChildrenParams(stateManager)
screenSlotNode.props._children = [screen.props]
const initialiseChildParams = getInitialiseParams(screenSlotNode)
const initialiseChildParams = getAttchChildrenParams(screenSlotNode)
attachChildren(initialiseChildParams)(screenSlotNode.rootElement, {
hydrate: true,
force: true,
})
if (currentScreenUbsubscribe) currentScreenUbsubscribe()
currentScreenUbsubscribe = unsubscribe
currentScreenStore = store
if (screenStateManager) screenStateManager.destroy()
screenStateManager = stateManager
currentUrl = url
}
@ -76,46 +48,28 @@ export const createApp = (
routeTo(currentUrl || window.location.pathname)
}
const attachChildrenParams = store => {
let currentState = null
const unsubscribe = store.subscribe(s => {
currentState = s
})
const attachChildrenParams = stateManager => {
const getInitialiseParams = treeNode => ({
bb: getBbClientApi,
coreApi,
store,
document,
componentLibraries,
frontendDefinition,
uiFunctions,
treeNode,
onScreenSlotRendered,
setupState: stateManager.setup,
getCurrentState: stateManager.getCurrentState,
})
const getBbClientApi = (treeNode, componentProps) => {
return {
attachChildren: attachChildren(getInitialiseParams(treeNode)),
context: treeNode.context,
props: componentProps,
call: safeCallEvent,
setStateFromBinding: (binding, value) =>
setStateFromBinding(store, binding, value),
setState: (path, value) => setState(store, path, value),
getStateOrValue: (prop, currentContext) =>
getStateOrValue(currentState, prop, currentContext),
store,
relativeUrl,
api,
isBound,
parent,
}
}
return { getInitialiseParams, unsubscribe }
return getInitialiseParams
}
let rootTreeNode
const pageStateManager = createStateManager({
store: writable({ _bbuser: user }),
coreApi,
frontendDefinition,
componentLibraries,
uiFunctions,
onScreenSlotRendered,
})
const initialisePage = (page, target, urlPath) => {
currentUrl = urlPath
@ -125,7 +79,7 @@ export const createApp = (
_children: [page.props],
}
rootTreeNode.rootElement = target
const { getInitialiseParams } = attachChildrenParams(pageStore)
const getInitialiseParams = attachChildrenParams(pageStateManager)
const initChildParams = getInitialiseParams(rootTreeNode)
attachChildren(initChildParams)(target, {
@ -137,8 +91,8 @@ export const createApp = (
}
return {
initialisePage,
screenStore: () => currentScreenStore,
pageStore: () => pageStore,
screenStore: () => screenStateManager.store,
pageStore: () => pageStateManager.store,
routeTo: () => routeTo,
rootNode: () => rootTreeNode,
}

4
packages/client/src/index.js

@ -43,12 +43,12 @@ export const loadBudibase = async (opts) => {
componentLibraries[builtinLibName] = builtins(_window)
const { initialisePage, screenStore, pageStore, routeTo, rootNode } = createApp(
_window.document,
componentLibraries,
frontendDefinition,
backendDefinition,
user,
uiFunctions || {}
uiFunctions || {},
_window
)
const route = _window.location

70
packages/client/src/render/attachChildren.js

@ -1,19 +1,17 @@
import { setupBinding } from "../state/stateBinding"
import { split, last } from "lodash/fp"
import { $ } from "../core/common"
import { renderComponent } from "./renderComponent"
import { prepareRenderComponent } from "./prepareRenderComponent"
import { isScreenSlot } from "./builtinComponents"
import deepEqual from "deep-equal"
export const attachChildren = initialiseOpts => (htmlElement, options) => {
const {
uiFunctions,
bb,
coreApi,
store,
componentLibraries,
treeNode,
frontendDefinition,
onScreenSlotRendered,
setupState,
getCurrentState,
} = initialiseOpts
const anchor = options && options.anchor ? options.anchor : null
@ -34,50 +32,46 @@ export const attachChildren = initialiseOpts => (htmlElement, options) => {
htmlElement.classList.add(`lay-${treeNode.props._id}`)
const renderedComponents = []
const childNodes = []
for (let childProps of treeNode.props._children) {
const { componentName, libName } = splitName(childProps._component)
if (!componentName || !libName) return
const { initialProps, bind } = setupBinding(
store,
childProps,
coreApi,
frontendDefinition.appRootPath
)
const componentConstructor = componentLibraries[libName][componentName]
const renderedComponentsThisIteration = renderComponent({
const childNodesThisIteration = prepareRenderComponent({
props: childProps,
parentNode: treeNode,
componentConstructor,
uiFunctions,
htmlElement,
anchor,
initialProps,
bb,
getCurrentState
})
if (
onScreenSlotRendered &&
isScreenSlot(childProps._component) &&
renderedComponentsThisIteration.length > 0
) {
// assuming there is only ever one screen slot
onScreenSlotRendered(renderedComponentsThisIteration[0])
for (let childNode of childNodesThisIteration) {
childNodes.push(childNode)
}
}
for (let comp of renderedComponentsThisIteration) {
comp.unsubscribe = bind(comp.component)
renderedComponents.push(comp)
}
if (areTreeNodesEqual(treeNode.children, childNodes)) return treeNode.children
for (let node of childNodes) {
const initialProps = setupState(node)
node.render(initialProps)
}
const screenSlot = childNodes.find(n => isScreenSlot(n.props._component))
if (onScreenSlotRendered && screenSlot) {
// assuming there is only ever one screen slot
onScreenSlotRendered(screenSlot)
}
treeNode.children = renderedComponents
treeNode.children = childNodes
return renderedComponents
return childNodes
}
const splitName = fullname => {
@ -90,3 +84,19 @@ const splitName = fullname => {
return { libName, componentName }
}
const areTreeNodesEqual = (children1, children2) => {
if (children1.length !== children2.length) return false
if (children1 === children2) return true
let isEqual = false
for (let i = 0; i < children1.length; i++) {
isEqual = deepEqual(children1[i].context, children2[i].context)
if (!isEqual) return false
if (isScreenSlot(children1[i].parentNode.props._component)) {
isEqual = deepEqual(children1[i].props, children2[i].props)
}
if (!isEqual) return false
}
return true
}

53
packages/client/src/render/renderComponent.js → packages/client/src/render/prepareRenderComponent.js

@ -1,22 +1,21 @@
export const renderComponent = ({
export const prepareRenderComponent = ({
componentConstructor,
uiFunctions,
htmlElement,
anchor,
props,
initialProps,
bb,
parentNode,
getCurrentState,
}) => {
const func = initialProps._id ? uiFunctions[initialProps._id] : undefined
const func = props._id ? uiFunctions[props._id] : undefined
const parentContext = (parentNode && parentNode.context) || {}
let renderedNodes = []
const render = context => {
let nodesToRender = []
const createNodeAndRender = context => {
let componentContext = parentContext
if (context) {
componentContext = { ...componentContext }
componentContext = { ...context }
componentContext.$parent = parentContext
}
@ -24,33 +23,31 @@ export const renderComponent = ({
thisNode.context = componentContext
thisNode.parentNode = parentNode
thisNode.props = props
nodesToRender.push(thisNode)
parentNode.children.push(thisNode)
renderedNodes.push(thisNode)
thisNode.render = initialProps => {
thisNode.component = new componentConstructor({
target: htmlElement,
props: initialProps,
hydrate: false,
anchor,
})
thisNode.rootElement =
htmlElement.children[htmlElement.children.length - 1]
initialProps._bb = bb(thisNode, props)
thisNode.component = new componentConstructor({
target: htmlElement,
props: initialProps,
hydrate: false,
anchor,
})
thisNode.rootElement = htmlElement.children[htmlElement.children.length - 1]
if (initialProps._id && thisNode.rootElement) {
thisNode.rootElement.classList.add(`pos-${initialProps._id}`)
if (props._id && thisNode.rootElement) {
thisNode.rootElement.classList.add(`pos-${props._id}`)
}
}
}
if (func) {
func(render, parentContext)
func(createNodeAndRender, parentContext, getCurrentState())
} else {
render()
createNodeAndRender()
}
return renderedNodes
return nodesToRender
}
export const createTreeNode = () => ({
@ -59,8 +56,10 @@ export const createTreeNode = () => ({
rootElement: null,
parentNode: null,
children: [],
bindings: [],
component: null,
unsubscribe: () => {},
render: () => {},
get destroy() {
const node = this
return () => {
@ -71,6 +70,10 @@ export const createTreeNode = () => ({
child.destroy()
}
}
for (let onDestroyItem of node.onDestroy) {
onDestroyItem()
}
}
},
onDestroy: [],
})

70
packages/client/src/state/bbComponentApi.js

@ -0,0 +1,70 @@
import { getStateOrValue } from "./getState"
import { setState, setStateFromBinding } from "./setState"
import { trimSlash } from "../common/trimSlash"
import { isBound } from "./isState"
import { attachChildren } from "../render/attachChildren"
export const bbFactory = ({
store,
getCurrentState,
frontendDefinition,
componentLibraries,
uiFunctions,
onScreenSlotRendered,
}) => {
const relativeUrl = url =>
frontendDefinition.appRootPath
? frontendDefinition.appRootPath + "/" + trimSlash(url)
: url
const apiCall = method => (url, body) =>
fetch(relativeUrl(url), {
method: method,
headers: {
"Content-Type": "application/json",
},
body: body && JSON.stringify(body),
})
const api = {
post: apiCall("POST"),
get: apiCall("GET"),
patch: apiCall("PATCH"),
delete: apiCall("DELETE"),
}
const safeCallEvent = (event, context) => {
const isFunction = obj =>
!!(obj && obj.constructor && obj.call && obj.apply)
if (isFunction(event)) event(context)
}
return (treeNode, setupState) => {
const attachParams = {
componentLibraries,
uiFunctions,
treeNode,
onScreenSlotRendered,
setupState,
getCurrentState,
}
return {
attachChildren: attachChildren(attachParams),
context: treeNode.context,
props: treeNode.props,
call: safeCallEvent,
setStateFromBinding: (binding, value) =>
setStateFromBinding(store, binding, value),
setState: (path, value) => setState(store, path, value),
getStateOrValue: (prop, currentContext) =>
getStateOrValue(getCurrentState(), prop, currentContext),
store: store,
relativeUrl,
api,
isBound,
parent,
}
}
}

3
packages/client/src/state/stateBinding.js

@ -23,7 +23,8 @@ const isMetaProp = propName =>
propName === "_component" ||
propName === "_children" ||
propName === "_id" ||
propName === "_style"
propName === "_style" ||
propName === "_code"
export const setupBinding = (store, rootProps, coreApi, context, rootPath) => {
const rootInitialProps = { ...rootProps }

288
packages/client/src/state/stateManager.js

@ -0,0 +1,288 @@
import {
isEventType,
eventHandlers,
EVENT_TYPE_MEMBER_NAME,
} from "./eventHandlers"
import { bbFactory } from "./bbComponentApi"
import { getState } from "./getState"
import { attachChildren } from "../render/attachChildren"
import {
isBound,
takeStateFromStore,
takeStateFromContext,
takeStateFromEventParameters,
BB_STATE_FALLBACK,
BB_STATE_BINDINGPATH,
BB_STATE_BINDINGSOURCE,
} from "./isState"
const doNothing = () => {}
doNothing.isPlaceholder = true
const isMetaProp = propName =>
propName === "_component" ||
propName === "_children" ||
propName === "_id" ||
propName === "_style" ||
propName === "_code" ||
propName === "_codeMeta"
export const createStateManager = ({
store,
coreApi,
rootPath,
frontendDefinition,
componentLibraries,
uiFunctions,
onScreenSlotRendered,
}) => {
let handlerTypes = eventHandlers(store, coreApi, rootPath)
let currentState
// any nodes that have props that are bound to the store
let nodesBoundByProps = []
// any node whose children depend on code, that uses the store
let nodesWithCodeBoundChildren = []
const getCurrentState = () => currentState
const registerBindings = _registerBindings(
nodesBoundByProps,
nodesWithCodeBoundChildren
)
const bb = bbFactory({
store,
getCurrentState,
frontendDefinition,
componentLibraries,
uiFunctions,
onScreenSlotRendered,
})
const setup = _setup(handlerTypes, getCurrentState, registerBindings, bb)
const unsubscribe = store.subscribe(
onStoreStateUpdated({
setCurrentState: s => (currentState = s),
getCurrentState,
nodesWithCodeBoundChildren,
nodesBoundByProps,
uiFunctions,
componentLibraries,
onScreenSlotRendered,
setupState: setup,
})
)
return {
setup,
destroy: () => unsubscribe(),
getCurrentState,
store,
}
}
const onStoreStateUpdated = ({
setCurrentState,
getCurrentState,
nodesWithCodeBoundChildren,
nodesBoundByProps,
uiFunctions,
componentLibraries,
onScreenSlotRendered,
setupState,
}) => s => {
setCurrentState(s)
// the original array gets changed by components' destroy()
// so we make a clone and check if they are still in the original
const nodesWithBoundChildren_clone = [...nodesWithCodeBoundChildren]
for (let node of nodesWithBoundChildren_clone) {
if (!nodesWithCodeBoundChildren.includes(node)) continue
attachChildren({
uiFunctions,
componentLibraries,
treeNode: node,
onScreenSlotRendered,
setupState,
getCurrentState,
})(node.rootElement, { hydrate: true, force: true })
}
for (let node of nodesBoundByProps) {
setNodeState(s, node)
}
}
const _registerBindings = (nodesBoundByProps, nodesWithCodeBoundChildren) => (
node,
bindings
) => {
if (bindings.length > 0) {
node.bindings = bindings
nodesBoundByProps.push(node)
const onDestroy = () => {
nodesBoundByProps = nodesBoundByProps.filter(n => n === node)
node.onDestroy = node.onDestroy.filter(d => d === onDestroy)
}
node.onDestroy.push(onDestroy)
}
if (
node.props._children &&
node.props._children.filter(c => c._codeMeta && c._codeMeta.dependsOnStore)
.length > 0
) {
nodesWithCodeBoundChildren.push(node)
const onDestroy = () => {
nodesWithCodeBoundChildren = nodesWithCodeBoundChildren.filter(
n => n === node
)
node.onDestroy = node.onDestroy.filter(d => d === onDestroy)
}
node.onDestroy.push(onDestroy)
}
}
const setNodeState = (storeState, node) => {
if (!node.component) return
const newProps = { ...node.bindings.initialProps }
for (let binding of node.bindings) {
const val = getState(storeState, binding.path, binding.fallback)
if (val === undefined && newProps[binding.propName] !== undefined) {
delete newProps[binding.propName]
}
if (val !== undefined) {
newProps[binding.propName] = val
}
}
node.component.$set(newProps)
}
const _setup = (
handlerTypes,
getCurrentState,
registerBindings,
bb
) => node => {
const props = node.props
const context = node.context || {}
const initialProps = { ...props }
const storeBoundProps = []
const currentStoreState = getCurrentState()
for (let propName in props) {
if (isMetaProp(propName)) continue
const val = props[propName]
if (isBound(val) && takeStateFromStore(val)) {
const path = BindingPath(val)
const source = BindingSource(val)
const fallback = BindingFallback(val)
storeBoundProps.push({
path,
fallback,
propName,
source,
})
initialProps[propName] = !currentStoreState
? fallback
: getState(
currentStoreState,
BindingPath(val),
BindingFallback(val),
BindingSource(val)
)
} else if (isBound(val) && takeStateFromContext(val)) {
initialProps[propName] = !context
? val
: getState(
context,
BindingPath(val),
BindingFallback(val),
BindingSource(val)
)
} else if (isEventType(val)) {
const handlersInfos = []
for (let e of val) {
const handlerInfo = {
handlerType: e[EVENT_TYPE_MEMBER_NAME],
parameters: e.parameters,
}
const resolvedParams = {}
for (let paramName in handlerInfo.parameters) {
const paramValue = handlerInfo.parameters[paramName]
if (!isBound(paramValue)) {
resolvedParams[paramName] = () => paramValue
continue
} else if (takeStateFromContext(paramValue)) {
const val = getState(
context,
paramValue[BB_STATE_BINDINGPATH],
paramValue[BB_STATE_FALLBACK]
)
resolvedParams[paramName] = () => val
} else if (takeStateFromStore(paramValue)) {
resolvedParams[paramName] = () =>
getState(
getCurrentState(),
paramValue[BB_STATE_BINDINGPATH],
paramValue[BB_STATE_FALLBACK]
)
continue
} else if (takeStateFromEventParameters(paramValue)) {
resolvedParams[paramName] = eventContext => {
getState(
eventContext,
paramValue[BB_STATE_BINDINGPATH],
paramValue[BB_STATE_FALLBACK]
)
}
}
}
handlerInfo.parameters = resolvedParams
handlersInfos.push(handlerInfo)
}
if (handlersInfos.length === 0) initialProps[propName] = doNothing
else {
initialProps[propName] = async context => {
for (let handlerInfo of handlersInfos) {
const handler = makeHandler(handlerTypes, handlerInfo)
await handler(context)
}
}
}
}
}
registerBindings(node, storeBoundProps)
const setup = _setup(handlerTypes, getCurrentState, registerBindings, bb)
initialProps._bb = bb(node, setup)
return initialProps
}
const makeHandler = (handlerTypes, handlerInfo) => {
const handlerType = handlerTypes[handlerInfo.handlerType]
return context => {
const parameters = {}
for (let p in handlerInfo.parameters) {
parameters[p] = handlerInfo.parameters[p](context)
}
handlerType.execute(parameters)
}
}
const BindingPath = prop => prop[BB_STATE_BINDINGPATH]
const BindingFallback = prop => prop[BB_STATE_FALLBACK]
const BindingSource = prop => prop[BB_STATE_BINDINGSOURCE]

174
packages/client/tests/bindingDom.spec.js

@ -1,4 +1,5 @@
import { load, makePage, makeScreen } from "./testAppDef"
import { EVENT_TYPE_MEMBER_NAME } from "../src/state/eventHandlers"
describe("initialiseApp (binding)", () => {
it("should populate root element prop from store value", async () => {
@ -169,4 +170,177 @@ describe("initialiseApp (binding)", () => {
"header 2 - new val"
)
})
it("should fire events", async () => {
const { dom, app } = await load(
makePage({
_component: "testlib/button",
onClick: [
event("Set State", {
path: "address",
value: "123 Main Street",
}),
],
})
)
const button = dom.window.document.body.children[0]
expect(button.tagName).toBe("BUTTON")
let storeAddress
app.pageStore().subscribe(s => {
storeAddress = s.address
})
button.dispatchEvent(new dom.window.Event("click"))
expect(storeAddress).toBe("123 Main Street")
})
it("should alter event parameters based on store values", async () => {
const { dom, app } = await load(
makePage({
_component: "testlib/button",
onClick: [
event("Set State", {
path: "address",
value: {
"##bbstate": "sourceaddress",
"##bbsource": "store",
"##bbstatefallback": "fallback address",
},
}),
],
})
)
const button = dom.window.document.body.children[0]
expect(button.tagName).toBe("BUTTON")
let storeAddress
app.pageStore().subscribe(s => {
storeAddress = s.address
})
button.dispatchEvent(new dom.window.Event("click"))
expect(storeAddress).toBe("fallback address")
app.pageStore().update(s => {
s.sourceaddress = "new address"
return s
})
button.dispatchEvent(new dom.window.Event("click"))
expect(storeAddress).toBe("new address")
})
it("should take event parameters from context values", async () => {
const { dom, app } = await load(
makePage({
_component: "testlib/button",
_id: "with_context",
onClick: [
event("Set State", {
path: "address",
value: {
"##bbstate": "testKey",
"##bbsource": "context",
"##bbstatefallback": "fallback address",
},
}),
],
})
)
const button = dom.window.document.body.children[0]
expect(button.tagName).toBe("BUTTON")
let storeAddress
app.pageStore().subscribe(s => {
storeAddress = s.address
})
button.dispatchEvent(new dom.window.Event("click"))
expect(storeAddress).toBe("test value")
})
})
it("should rerender components when their code is bound to the store ", async () => {
const { dom, app } = await load(
makePage({
_component: "testlib/div",
_children: [
{
_component: "testlib/div",
_id: "n_clones_based_on_store",
className: "child_div",
},
],
})
)
const rootDiv = dom.window.document.body.children[0]
expect(rootDiv.tagName).toBe("DIV")
expect(rootDiv.children.length).toBe(0)
app.pageStore().update(s => {
s.componentCount = 3
return s
})
expect(rootDiv.children.length).toBe(3)
expect(rootDiv.children[0].className.includes("child_div")).toBe(true)
app.pageStore().update(s => {
s.componentCount = 5
return s
})
expect(rootDiv.children.length).toBe(5)
expect(rootDiv.children[0].className.includes("child_div")).toBe(true)
app.pageStore().update(s => {
s.componentCount = 0
return s
})
expect(rootDiv.children.length).toBe(0)
})
it("should be able to read value from context, passed fromm parent, through code", async () => {
const { dom, app } = await load(
makePage({
_component: "testlib/div",
_children: [
{
_component: "testlib/div",
_id: "n_clones_based_on_store",
className: {
"##bbstate": "index",
"##bbsource": "context",
"##bbstatefallback": "nothing",
},
},
],
})
)
const rootDiv = dom.window.document.body.children[0]
expect(rootDiv.tagName).toBe("DIV")
expect(rootDiv.children.length).toBe(0)
app.pageStore().update(s => {
s.componentCount = 3
return s
})
expect(rootDiv.children.length).toBe(3)
expect(rootDiv.children[0].className.includes("index_0")).toBe(true)
expect(rootDiv.children[1].className.includes("index_1")).toBe(true)
expect(rootDiv.children[2].className.includes("index_2")).toBe(true)
})
const event = (handlerType, parameters) => {
const e = {}
e[EVENT_TYPE_MEMBER_NAME] = handlerType
e.parameters = parameters
return e
}

58
packages/client/tests/testAppDef.js

@ -15,6 +15,7 @@ export const load = async (page, screens = [], url = "/") => {
actions: [],
triggers: [],
})
setComponentCodeMeta(page, screens)
const app = await loadBudibase({
componentLibraries: allLibs(dom.window),
window: dom.window,
@ -47,8 +48,11 @@ export const timeout = ms => new Promise(resolve => setTimeout(resolve, ms))
export const walkComponentTree = (node, action) => {
action(node)
if (node.children) {
for (let child of node.children) {
// works for nodes or props
const children = node.children || node._children
if (children) {
for (let child of children) {
walkComponentTree(child, action)
}
}
@ -68,6 +72,22 @@ const autoAssignIds = (props, count = 0) => {
}
}
// any component with an id that include "based_on_store" is
// assumed to have code that depends on store value
const setComponentCodeMeta = (page, screens) => {
const setComponentCodeMeta_single = props => {
walkComponentTree(props, c => {
if (c._id.indexOf("based_on_store") >= 0) {
c._codeMeta = { dependsOnStore: true }
}
})
}
setComponentCodeMeta_single(page.props)
for (let s of screens || []) {
setComponentCodeMeta_single(s.props)
}
}
const setAppDef = (window, page, screens) => {
window["##BUDIBASE_FRONTEND_DEFINITION##"] = {
componentLibraries: [],
@ -148,6 +168,29 @@ const maketestlib = window => ({
set(opts.props)
opts.target.appendChild(node)
},
button: function(opts) {
const node = window.document.createElement("BUTTON")
let currentProps = { ...opts.props }
const set = props => {
currentProps = Object.assign(currentProps, props)
if (currentProps.onClick) {
node.addEventListener("click", () => {
const testText = currentProps.testText || "hello"
currentProps._bb.call(props.onClick, { testText })
})
}
}
this.$destroy = () => opts.target.removeChild(node)
this.$set = set
this._element = node
set(opts.props)
opts.target.appendChild(node)
},
})
const uiFunctions = {
@ -162,4 +205,15 @@ const uiFunctions = {
render()
}
},
with_context: render => {
render({ testKey: "test value" })
},
n_clones_based_on_store: (render, _, state) => {
const n = state.componentCount || 0
for (let i = 0; i < n; i++) {
render({ index: `index_${i}` })
}
},
}

185
packages/materialdesign-components/components.json

@ -1,20 +1,189 @@
{
"_lib": "./dist/index.js",
"h1": {
"name": "H1",
"description": "An HTML H1 tag",
"_generators": {},
"Body1": {
"name": "Body1",
"description": "Sets the font properties as Roboto Body 1",
"props": {
"text": "string"
},
"tags": []
},
"Body2": {
"name": "Body2",
"description": "Sets the font properties as Roboto Body 2",
"props": {
"text": "string"
},
"tags": []
},
"Button": {
"name": "Button",
"description": "A Material Design button with different variations. It renders as an anchor if href is passed to it.",
"props": {
"onClick": "event",
"variant": "string",
"colour": "string",
"size": "string",
"href": "string",
"icon": "string",
"trailingIcon": "bool",
"fullwidth": "bool",
"text": "string",
"className": "string"
"disabled": "bool"
},
"tags": []
},
"button": {
"name": "Button",
"description": "A button",
"Caption": {
"name": "Caption",
"description": "Sets the font properties as Roboto Caption",
"props": {
"text": "string"
},
"tags": []
},
"Checkbox": {
"name": "Checkbox",
"description": "A Material Design checkbox. Supports aligning label before or after checkbox.",
"props": {
"onClick": "event",
"id": "string",
"label": "string",
"disabled": "bool",
"alignEnd": "bool",
"indeterminate": "bool",
"checked": "bool"
},
"tags": []
},
"Datatable": {
"name": "Datatable",
"description": "A Material Design component to represent tabular data.",
"props": {},
"tags": []
},
"H1": {
"name": "H1",
"description": "Sets the font properties as Roboto Headline1",
"props": {
"text": "string"
},
"tags": []
},
"H2": {
"name": "H2",
"description": "Sets the font properties as Roboto Headline2",
"props": {
"text": "string"
},
"tags": []
},
"H3": {
"name": "H3",
"description": "Sets the font properties as Roboto Headline3",
"props": {
"text": "string"
},
"tags": []
},
"H4": {
"name": "H4",
"description": "Sets the font properties as Roboto Headline4",
"props": {
"text": "string"
},
"tags": []
},
"H5": {
"name": "H5",
"description": "Sets the font properties as Roboto Headline5",
"props": {
"text": "string"
},
"tags": []
},
"H6": {
"name": "H6",
"description": "Sets the font properties as Roboto Headline6",
"props": {
"text": "string"
},
"tags": []
},
"Label": {
"name": "Label",
"description": "A simple label component that displays its text in the standard Roboto Material Design font",
"props": {
"bold": "bool"
},
"tags": []
},
"Overline": {
"name": "Overline",
"description": "Sets the font properties as Roboto Overline",
"props": {
"text": "string"
},
"tags": []
},
"Radiobutton": {
"name": "Radiobutton",
"description": "A Material Design radiobutton. Supports aligning label before or after radiobutton.",
"props": {
"onClick": "event",
"id": "string",
"label": "string",
"names": "string",
"name": "string",
"checked": "bool",
"disabled": "bool",
"alignEnd": "bool"
},
"tags": []
},
"Sub1": {
"name": "Sub1",
"description": "Sets the font properties as Roboto Subtitle1",
"props": {
"text": "string"
},
"tags": []
},
"Sub2": {
"name": "Sub2",
"description": "Sets the font properties as Roboto Subtitle2",
"props": {
"text": "string"
},
"tags": []
},
"Textfield": {
"name": "Textfield",
"description": "A Material Design textfield with multiple variants. Can also be converted to a text area / multine text field.",
"props": {
"raised": "bool"
"onChange": "event",
"label": "string",
"variant": "string",
"disabled": "bool",
"fullwidth": "bool",
"colour":"string",
"size":"string",
"type": "string",
"required": "bool",
"minLength": "number",
"maxLength": "number",
"helperText": "string",
"errorText": "string",
"placeholder": "string",
"icon": "string",
"trailingIcon": "bool",
"textarea": "bool",
"rows": "number",
"cols": "number",
"validation": "bool",
"persistent": "bool"
},
"tags": []
}
}

1
packages/materialdesign-components/package.json

@ -41,6 +41,7 @@
"gitHead": "115189f72a850bfb52b65ec61d932531bf327072",
"dependencies": {
"@material/checkbox": "^4.0.0",
"@material/data-table": "4.0.0",
"@material/form-field": "^4.0.0",
"@material/radio": "^4.0.0",
"@material/textfield": "^4.0.0"

12
packages/materialdesign-components/src/Button.svelte

@ -1,12 +0,0 @@
<script>
import "@material/button/mdc-button.scss"
export let raised = false
let c = raised ? "mdc-button mdc-button--raised" : "mdc-button"
</script>
<button class={c}>
<div class="mdc-button__ripple" />
<span class="mdc-button__label">Button</span>
</button>

4
packages/materialdesign-components/src/Button/Button.svelte

@ -1,7 +1,7 @@
<script>
import { setContext, getContext } from "svelte"
import Icon from "../Icon.svelte"
import ripple from "../Ripple.js"
import Icon from "../Common/Icon.svelte"
import ripple from "../Common/Ripple.js"
import ClassBuilder from "../ClassBuilder.js"
const cb = new ClassBuilder("button", ["primary", "medium"])

2
packages/materialdesign-components/src/Button/index.js

@ -1,2 +1,2 @@
import "./_index.scss"
export { default as button } from "./Button.svelte"
export { default as Button } from "./Button.svelte"

4
packages/materialdesign-components/src/Checkbox/index.js

@ -1,4 +1,4 @@
import "./_style.scss";
export { default as checkbox } from "./Checkbox.svelte";
export { default as checkboxgroup } from "./CheckboxGroup.svelte";
export { default as Checkbox } from "./Checkbox.svelte";
export { default as Checkboxgroup } from "./CheckboxGroup.svelte";

0
packages/materialdesign-components/src/Icon.svelte → packages/materialdesign-components/src/Common/Icon.svelte

0
packages/materialdesign-components/src/Ripple.js → packages/materialdesign-components/src/Common/Ripple.js

67
packages/materialdesign-components/src/Datatable/Datatable.svelte

@ -0,0 +1,67 @@
<script>
import { onMount, setContext } from "svelte"
import { MDCDataTable } from "@material/data-table"
import Row from "./DatatableRow.svelte"
import Cell from "./DatatableCell.svelte"
import { Button } from "../Button"
import ClassBuilder from "../ClassBuilder.js"
const cb = new ClassBuilder("data-table")
setContext("BBMD:data-table:cb", cb)
let datatable = null
let instance = null
onMount(() => {
if (!!datatable) instance = new MDCDataTable(datatable)
return () => {
!!instance && instance.destroy()
instance = null
}
})
</script>
<div bind:this={datatable} class={cb.build()}>
<table class={cb.elem`table`} aria-label="Material Design Datatable">
<thead>
<Row isHeader>
<Cell isHeader>Id</Cell>
<Cell isHeader>First Name</Cell>
<Cell isHeader>Second Name</Cell>
<Cell isHeader>Gender</Cell>
<Cell isHeader>Address</Cell>
<Cell isHeader>Actions</Cell>
</Row>
</thead>
<tbody class={cb.elem`content`}>
<Row>
<Cell>123456</Cell>
<Cell>Conor</Cell>
<Cell>McKeown</Cell>
<Cell>Male</Cell>
<Cell>1 Cool Street</Cell>
<Cell>
<Button
text="Select"
variant="unelevated"
colour="secondary"
size="small" />
</Cell>
</Row>
<Row>
<Cell>789101</Cell>
<Cell>Joe</Cell>
<Cell>Bloggs</Cell>
<Cell>Male</Cell>
<Cell>2 Cool Street</Cell>
<Cell>
<Button
text="Select"
variant="unelevated"
colour="secondary"
size="small" />
</Cell>
</Row>
</tbody>
</table>
</div>

23
packages/materialdesign-components/src/Datatable/DatatableCell.svelte

@ -0,0 +1,23 @@
<script>
import { getContext } from "svelte"
export let isHeader = false
export let numeric = false
const cb = getContext("BBMD:data-table:cb")
let elementName = isHeader ? "header-cell" : "cell"
let modifiers = { numeric }
let props = { modifiers }
let cellClass = cb.build({ elementName, props })
</script>
{#if isHeader}
<th class={cellClass} role="columnheader" scope="col">
<slot />
</th>
{:else}
<td class={cellClass}>
<slot />
</td>
{/if}

26
packages/materialdesign-components/src/Datatable/DatatableRow.svelte

@ -0,0 +1,26 @@
<script>
import { getContext } from "svelte";
export let onSelect = () => {};
export let isHeader = false;
let row = null;
let selected = false;
const cb = getContext("BBMD:data-table:cb");
let elementName = isHeader ? "header-row" : "row";
let modifiers = {};
$: modifiers = { selected };
$: props = { modifiers };
$: rowClass = cb.build({ elementName, props });
function rowSelected() {
selected = !selected;
onSelect();
}
</script>
<tr bind:this={row} class={rowClass} on:click={rowSelected}>
<slot />
</tr>

1
packages/materialdesign-components/src/Datatable/_style.scss

@ -0,0 +1 @@
@import "@material/data-table/mdc-data-table";

2
packages/materialdesign-components/src/Datatable/index.js

@ -0,0 +1,2 @@
import "./_style.scss";
export { default as Datatable } from "./Datatable.svelte";

8
packages/materialdesign-components/src/H1.svelte

@ -1,8 +0,0 @@
<script>
export let text = ""
export let className = ""
export let _bb
</script>
<h1 class={className}>{text}</h1>

4
packages/materialdesign-components/src/Radiobutton/Radiobutton.svelte

@ -8,7 +8,7 @@
export let id = ""
export let label = ""
export let names = "radios"
export let name = "radios"
export let checked = false
export let disabled = false
export let alignEnd = false
@ -37,7 +37,7 @@
{id}
class={cb.elem`native-control`}
type="radio"
{names}
{name}
{checked}
{disabled}
on:click={onClick} />

4
packages/materialdesign-components/src/Radiobutton/index.js

@ -1,3 +1,3 @@
import "./_style.scss";
export { default as radiobutton } from "./Radiobutton.svelte";
export { default as radiobuttongroup } from "./RadiobuttonGroup.svelte";
export { default as Radiobutton } from "./Radiobutton.svelte";
export { default as Radiobuttongroup } from "./RadiobuttonGroup.svelte";

34
packages/materialdesign-components/src/Test/TestApp.svelte

@ -3,14 +3,15 @@
import { props } from "./props"
let _bb
const {
h1,
overline,
button,
textfield,
checkbox,
checkboxgroup,
radiobutton,
radiobuttongroup,
H1,
Overline,
Button,
Textfield,
Checkbox,
Checkboxgroup,
Radiobutton,
Radiobuttongroup,
Datatable,
} = props
let currentComponent
@ -22,14 +23,15 @@
props: {
_component: "testcomponents/rootComponent",
_children: [
h1,
overline,
button,
textfield,
checkbox,
checkboxgroup,
radiobutton,
radiobuttongroup,
H1,
Overline,
Button,
Textfield,
Checkbox,
Checkboxgroup,
Radiobutton,
Radiobuttongroup,
Datatable,
],
},
}

38
packages/materialdesign-components/src/Test/props.js

@ -1,17 +1,17 @@
export const props = {
h1: {
H1: {
_component: "@budibase/materialdesign-components/H1",
_children: [],
text: "Im a big header",
},
overline: {
Overline: {
_component: "@budibase/materialdesign-components/Overline",
_children: [],
text: "Im a wee overline",
},
button: {
_component: "@budibase/materialdesign-components/button",
Button: {
_component: "@budibase/materialdesign-components/Button",
_children: [],
variant: "raised",
colour: "secondary",
@ -24,13 +24,13 @@ export const props = {
disabled: false,
onClick: () => alert`Button Clicked`,
},
icon: {
_component: "@budibase/materialdesign-components/icon",
Icon: {
_component: "@budibase/materialdesign-components/Icon",
_children: [],
icon: "",
},
textfield: {
_component: "@budibase/materialdesign-components/textfield",
Textfield: {
_component: "@budibase/materialdesign-components/Textfield",
_children: [],
label: "First",
colour: "secondary",
@ -39,15 +39,15 @@ export const props = {
helperText: "Add Surname",
onChange: text => console.log("Text: ", text),
},
checkbox: {
_component: "@budibase/materialdesign-components/checkbox",
Checkbox: {
_component: "@budibase/materialdesign-components/Checkbox",
_children: [],
id: "test-check",
label: "Check Yo Self",
onClick: () => alert`Before ya reck yo'self`,
},
checkboxgroup: {
_component: "@budibase/materialdesign-components/checkboxgroup",
Checkboxgroup: {
_component: "@budibase/materialdesign-components/Checkboxgroup",
_children: [],
label: "Whats your favourite?",
items: [
@ -57,15 +57,15 @@ export const props = {
],
onChange: selectedItems => console.log(selectedItems),
},
radiobutton: {
_component: "@budibase/materialdesign-components/radiobutton",
Radiobutton: {
_component: "@budibase/materialdesign-components/Radiobutton",
_children: [],
label: "Hi radio",
alignEnd: true,
onClick: () => alert`Roger That`,
},
radiobuttongroup: {
_component: "@budibase/materialdesign-components/radiobuttongroup",
Radiobuttongroup: {
_component: "@budibase/materialdesign-components/Radiobuttongroup",
_children: [],
label: "Preferred method of contact: ",
orientation: "column",
@ -75,5 +75,9 @@ export const props = {
{ label: "Social Media", value: 3 },
],
onChange: selected => console.log(selected),
}
},
Datatable: {
_component: "@budibase/materialdesign-components/Datatable",
_children: [],
},
}

30
packages/materialdesign-components/src/Test/testComponents.js

@ -1,23 +1,25 @@
import {
H1,
Overline,
button,
icon,
textfield,
checkbox,
checkboxgroup,
radiobutton,
radiobuttongroup,
Button,
Icon,
Textfield,
Checkbox,
Checkboxgroup,
Radiobutton,
Radiobuttongroup,
Datatable,
} from "@BBMD"
export default {
H1,
Overline,
button,
icon,
textfield,
checkbox,
checkboxgroup,
radiobutton,
radiobuttongroup,
Button,
Icon,
Textfield,
Checkbox,
Checkboxgroup,
Radiobutton,
Radiobuttongroup,
Datatable,
}

2
packages/materialdesign-components/src/Textfield/Textfield.svelte

@ -8,7 +8,7 @@
import FloatingLabel from "../Common/FloatingLabel.svelte"
import HelperText from "./HelperText.svelte"
import CharacterCounter from "./CharacterCounter.svelte"
import Icon from "../Icon.svelte"
import Icon from "../Common/Icon.svelte"
const cb = new ClassBuilder("text-field", ["primary", "medium"])

2
packages/materialdesign-components/src/Textfield/index.js

@ -1,2 +1,2 @@
import "./_index.scss"
export { default as textfield } from "./Textfield.svelte"
export { default as Textfield } from "./Textfield.svelte"

8
packages/materialdesign-components/src/Typography/index.js

@ -1,13 +1,13 @@
import "./_style.scss";
export { default as Body1 } from "./Body1.svelte";
export { default as Body2 } from "./Body2.svelte";
export { default as Caption } from "./Caption.svelte";
export { default as H1 } from "./H1.svelte";
export { default as H2 } from "./H2.svelte";
export { default as H3 } from "./H3.svelte";
export { default as H4 } from "./H4.svelte";
export { default as H5 } from "./H5.svelte";
export { default as H6 } from "./H6.svelte";
export { default as Body1 } from "./Body1.svelte";
export { default as Body2 } from "./Body2.svelte";
export { default as Overline } from "./Overline.svelte";
export { default as Sub1 } from "./Sub1.svelte";
export { default as Sub2 } from "./Sub2.svelte";
export { default as Caption } from "./Caption.svelte";
export { default as Overline } from "./Overline.svelte";

12
packages/materialdesign-components/src/index.js

@ -1,9 +1,11 @@
import "@material/theme/mdc-theme.scss";
export { button } from "./Button"
export { default as icon } from "./Icon.svelte"
export { textfield } from "./Textfield"
export { Button } from "./Button"
export { default as Icon } from "./Common/Icon.svelte"
export { Textfield } from "./Textfield"
export * from "./Typography"
export { checkbox, checkboxgroup } from "./Checkbox"
export { radiobutton, radiobuttongroup } from "./Radiobutton"
export { Checkbox, Checkboxgroup } from "./Checkbox"
export { Radiobutton, Radiobuttongroup } from "./Radiobutton"
export { default as Label } from "./Common/Label.svelte"
export { Datatable } from "./Datatable"

92
packages/materialdesign-components/src/scripts/publishDev.js

@ -0,0 +1,92 @@
const { readdir, stat, copyFile } = require("fs-extra")
const { constants } = require("fs")
const { join, basename } = require("path")
const serverConfig = require("../../../server/config")()
const packagesFolder = ".."
const jsFile = dir => join(dir, "index.js")
const generatorsFile = dir => join(dir, "generators.js")
const jsMapFile = dir => join(dir, "index.js.map")
const sourceJs = jsFile("dist")
const sourceJsMap = jsMapFile("dist")
const componentsFile = "components.json"
const sourceGenerators = generatorsFile("dist")
const appPackages = join(
packagesFolder,
"server",
serverConfig.latestPackagesFolder
)
const publicMain = appName =>
join(
appPackages,
appName,
"public",
"main",
"lib",
"node_modules",
"@budibase",
"standard-components"
)
const publicUnauth = appName =>
join(
appPackages,
appName,
"public",
"unauthenticated",
"lib",
"node_modules",
"@budibase",
"standard-components"
)
const nodeModulesDist = appName =>
join(
appPackages,
appName,
"node_modules",
"@budibase",
"standard-components",
"dist"
)
const nodeModules = appName =>
join(appPackages, appName, "node_modules", "@budibase", "standard-components")
;(async () => {
const apps = await readdir(appPackages)
const copySource = file => async toDir => {
const dest = join(toDir, basename(file))
try {
await copyFile(file, dest, constants.COPYFILE_FICLONE)
console.log(`COPIED ${file} to ${dest}`)
} catch (e) {
console.log(`COPY FAILED ${file} to ${dest}: ${e}`)
}
}
const copySourceJs = copySource(sourceJs)
const copySourceJsMap = copySource(sourceJsMap)
const copyGenerators = copySource(sourceGenerators)
const copyComponentsJson = copySource(componentsFile)
for (let app of apps) {
if (app === ".data") continue
if (!(await stat(join(appPackages, app))).isDirectory()) continue
await copySourceJs(nodeModulesDist(app))
await copySourceJsMap(nodeModulesDist(app))
await copyGenerators(nodeModulesDist(app))
await copyComponentsJson(nodeModules(app))
await copySourceJs(join(publicMain(app), "dist"))
await copySourceJsMap(join(publicMain(app), "dist"))
await copyGenerators(join(publicMain(app), "dist"))
await copySourceJs(join(publicUnauth(app), "dist"))
await copySourceJsMap(join(publicUnauth(app), "dist"))
await copyGenerators(join(publicUnauth(app), "dist"))
}
})()

3
packages/server/utilities/builder/buildPage.js

@ -14,6 +14,7 @@ const { join, resolve, dirname } = require("path")
const sqrl = require("squirrelly")
const { convertCssToFiles } = require("./convertCssToFiles")
const publicPath = require("./publicPath")
const deleteCodeMeta = require("./deleteCodeMeta")
module.exports = async (config, appname, pageName, pkg) => {
const appPath = appPackageFolder(config, appname)
@ -155,6 +156,8 @@ const savePageJson = async (appPath, pageName, pkg) => {
delete pkg.page._screens
}
deleteCodeMeta(pkg.page.props)
await writeJSON(pageFile, pkg.page, {
spaces: 2,
})

9
packages/server/utilities/builder/deleteCodeMeta.js

@ -0,0 +1,9 @@
module.exports = props => {
if (props._codeMeta) {
delete props._codeMeta
}
for (let child of props._children || []) {
module.exports(child)
}
}

6
packages/server/utilities/builder/index.js

@ -18,6 +18,7 @@ const buildPage = require("./buildPage")
const getPages = require("./getPages")
const listScreens = require("./listScreens")
const saveBackend = require("./saveBackend")
const deleteCodeMeta = require("./deleteCodeMeta")
module.exports.buildPage = buildPage
module.exports.listScreens = listScreens
@ -58,12 +59,15 @@ module.exports.saveScreen = async (config, appname, pagename, screen) => {
if (screen._css) {
delete screen._css
}
deleteCodeMeta(screen.props)
await writeJSON(compPath, screen, {
encoding: "utf8",
flag: "w",
spaces: 2,
})
return screen;
return screen
}
module.exports.renameScreen = async (

4
packages/standard-components/src/button.svelte

@ -34,7 +34,7 @@
return all
}
$: if (_bb.props._children.length > 0)
$: if(_bb.props._children && _bb.props._children.length > 0)
theButton && _bb.attachChildren(theButton)
$: {
@ -74,7 +74,7 @@
disabled={disabled || false}
on:click={clickHandler}
style={buttonStyles}>
{#if _bb.props._children.length === 0}{contentText}{/if}
{#if !_bb.props._children || _bb.props._children.length === 0}{contentText}{/if}
</button>
<style>

Loading…
Cancel
Save