Skip to content

Commit 68e98cc

Browse files
authored
Merge pull request #149 from ruchamahabal/two-way-binding
2 parents 2d498a2 + 85e40cf commit 68e98cc

File tree

9 files changed

+144
-166
lines changed

9 files changed

+144
-166
lines changed

frontend/src/components/AppComponent.vue

Lines changed: 26 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,10 @@
1010
v-show="showComponent"
1111
:is="componentName"
1212
v-bind="componentProps"
13-
v-model="boundValue"
13+
v-on="{ ...vModelListeners, ...componentEvents }"
1414
:data-component-id="block.componentId"
1515
:style="styles"
1616
:class="classes"
17-
v-on="componentEvents"
1817
>
1918
<!-- Dynamically render named slots -->
2019
<template v-for="(slot, slotName) in block.componentSlots" :key="slotName" v-slot:[slotName]>
@@ -35,7 +34,7 @@
3534

3635
<script setup lang="ts">
3736
import Block from "@/utils/block"
38-
import { computed, onMounted, ref, useAttrs, inject, type ComputedRef, toRefs } from "vue"
37+
import { computed, onMounted, ref, useAttrs, inject, type ComputedRef } from "vue"
3938
import type { ComponentPublicInstance } from "vue"
4039
import { useRouter } from "vue-router"
4140
import { createResource } from "frappe-ui"
@@ -92,14 +91,34 @@ const getComponentProps = () => {
9291
if (!props.block || props.block.isRoot()) return []
9392
9493
const propValues = props.block.getPropsAndAttributes()
95-
delete propValues.modelValue
96-
97-
Object.entries(propValues).forEach(([propName, config]) => {
98-
propValues[propName] = codeStore.evaluateDynamicValues(config, evaluationContext.value)
94+
Object.entries(propValues).forEach(([propName, propValue]) => {
95+
if (propValue?.$type === "variable") {
96+
propValues[propName] = codeStore.getValueFromVariable(propValue.name, evaluationContext.value)
97+
} else if (isDynamicValue(propValue)) {
98+
propValues[propName] = codeStore.evaluateDynamicValues(propValue, evaluationContext.value)
99+
}
99100
})
100101
return propValues
101102
}
102103
104+
// 2-way binding
105+
const vModelListeners = computed(() => {
106+
if (!props.block || props.block.isRoot()) return {}
107+
108+
const listeners: Record<string, Function> = {}
109+
const propValues = props.block.getPropsAndAttributes()
110+
111+
Object.entries(propValues).forEach(([propName, propValue]) => {
112+
if (propValue?.$type === "variable") {
113+
const eventName = `update:${propName}`
114+
listeners[eventName] = (newValue: any) => {
115+
codeStore.setValueInVariable(propValue.name, newValue, evaluationContext.value)
116+
}
117+
}
118+
})
119+
return listeners
120+
})
121+
103122
const attrs = useAttrs()
104123
const componentProps = computed(() => {
105124
return {
@@ -116,28 +135,6 @@ const showComponent = computed(() => {
116135
return true
117136
})
118137
119-
// modelValue binding
120-
const boundValue = computed({
121-
get() {
122-
const modelValue = props.block.componentProps.modelValue
123-
if (modelValue?.$type === "variable") {
124-
return codeStore.getValueFromVariable(modelValue.name)
125-
} else if (isDynamicValue(modelValue)) {
126-
return codeStore.getDynamicValue(modelValue, evaluationContext.value)
127-
}
128-
return modelValue
129-
},
130-
set(newValue) {
131-
const modelValue = props.block.componentProps.modelValue
132-
if (modelValue?.$type === "variable") {
133-
codeStore.setValueInVariable(modelValue.name, newValue)
134-
} else {
135-
// update the prop directly if not bound to a variable
136-
props.block.setProp("modelValue", newValue)
137-
}
138-
},
139-
})
140-
141138
// events
142139
const router = useRouter()
143140

frontend/src/components/AppLayout/Repeater.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
>
77
{{ emptyStateMessage || "No data" }}
88
</div>
9-
<template v-else v-for="(dataItem, dataIndex) in data" :key="dataItem?.[dataKey]">
9+
<template v-else v-for="(dataItem, dataIndex) in data" :key="dataItem?.[dataKey] || dataIndex">
1010
<RepeaterContextProvider :dataItem="dataItem" :dataIndex="dataIndex" :dataKey="dataKey">
1111
<slot v-bind="{ dataItem, dataKey, dataIndex }"></slot>
1212
</RepeaterContextProvider>

frontend/src/components/DynamicValueSelector.vue

Lines changed: 46 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,30 +3,48 @@
33
size="sm"
44
:options="dynamicValueOptions"
55
class="!w-auto"
6+
placement="left-start"
67
modelValue=""
7-
@update:modelValue="(option: VariableOption) => emit('update:modelValue', option.value)"
8+
@update:modelValue="(option: VariableOption) => emit('update:modelValue', option.value, bindVariable)"
89
>
910
<template #target="{ togglePopover }">
10-
<slot name="target" v-if="$slots.target" v-bind="{ togglePopover }"></slot>
11-
<Tooltip v-else text="Click to set dynamic value" placement="bottom">
12-
<FeatherIcon
13-
ref="dropdownTrigger"
14-
name="plus-circle"
15-
class="mr-1 h-3 w-4 cursor-pointer select-none text-ink-gray-5 outline-none hover:text-ink-gray-9"
16-
@click="togglePopover"
17-
/>
18-
</Tooltip>
11+
<IconButton
12+
v-if="bindVariable"
13+
:icon="Link2"
14+
label="Synced with variable. Click to change."
15+
placement="bottom"
16+
class="mr-1"
17+
@click="togglePopover"
18+
/>
19+
<IconButton
20+
v-else
21+
icon="plus-circle"
22+
label="Click to set dynamic value"
23+
placement="bottom"
24+
class="mr-1"
25+
size="sm"
26+
@click="togglePopover"
27+
/>
1928
</template>
2029

2130
<template #item-suffix="{ option }">
2231
<span class="text-ink-gray-4">{{ option.type?.toLowerCase() }}</span>
2332
</template>
33+
<template #footer>
34+
<div class="flex items-center p-2" @mousedown.prevent>
35+
<Tooltip text="Changing the selected variable value will change the prop value and vice versa">
36+
<FeatherIcon name="info" class="size-3 text-ink-gray-5" />
37+
</Tooltip>
38+
<Switch v-model="bindVariable" label="Sync with variable" class="w-full" />
39+
</div>
40+
</template>
2441
</Autocomplete>
2542
</template>
2643

2744
<script setup lang="ts">
28-
import { computed } from "vue"
29-
import { Autocomplete, Tooltip } from "frappe-ui"
45+
import { computed, ref, watch } from "vue"
46+
import { Autocomplete, Switch, Tooltip } from "frappe-ui"
47+
import IconButton from "@/components/IconButton.vue"
3048
import useStudioStore from "@/stores/studioStore"
3149
import useCanvasStore from "@/stores/canvasStore"
3250
import useComponentEditorStore from "@/stores/componentEditorStore"
@@ -35,24 +53,25 @@ import type { VariableOption } from "@/types/Studio/StudioPageVariable"
3553
import type { ComponentInput } from "@/types/Studio/StudioComponent"
3654
import { isObjectEmpty } from "@/utils/helpers"
3755
import useCodeStore from "@/stores/codeStore"
56+
import Link2 from "~icons/lucide/link-2"
3857
39-
const props = withDefaults(defineProps<{ block?: Block; formatValuesAsTemplate?: boolean }>(), {
40-
formatValuesAsTemplate: true,
41-
})
58+
const props = defineProps<{ block?: Block; isVariableBound?: string | null }>()
4259
const emit = defineEmits<{
43-
(event: "update:modelValue", value: string): void
60+
(event: "update:modelValue", value: string, bindVariable: boolean): void
4461
}>()
62+
const bindVariable = ref(!!props.isVariableBound)
63+
64+
watch(
65+
() => props.isVariableBound,
66+
(newValue) => {
67+
bindVariable.value = !!newValue
68+
},
69+
)
70+
4571
const store = useStudioStore()
4672
const canvasStore = useCanvasStore()
4773
const codeStore = useCodeStore()
4874
49-
const formatValue = (value: string) => {
50-
if (props.formatValuesAsTemplate) {
51-
return `{{ ${value} }}`
52-
}
53-
return value
54-
}
55-
5675
const dynamicValueOptions = computed(() => {
5776
const groups = []
5877
@@ -63,7 +82,7 @@ const dynamicValueOptions = computed(() => {
6382
const componentContext: VariableOption[] = []
6483
componentInputs.map?.((input: ComponentInput) => {
6584
componentContext.push({
66-
value: formatValue(`inputs.${input.input_name}`),
85+
value: `inputs.${input.input_name}`,
6786
label: `inputs.${input.input_name}`,
6887
type: input.type,
6988
})
@@ -78,10 +97,7 @@ const dynamicValueOptions = computed(() => {
7897
if (store.variableOptions.length > 0) {
7998
groups.push({
8099
group: "Variables",
81-
items: store.variableOptions.map((option) => ({
82-
...option,
83-
value: formatValue(option.value),
84-
})),
100+
items: store.variableOptions,
85101
})
86102
}
87103
// Data Sources group
@@ -91,7 +107,7 @@ const dynamicValueOptions = computed(() => {
91107
? `${resourceName}.doc`
92108
: `${resourceName}.data`
93109
return {
94-
value: formatValue(completion),
110+
value: completion,
95111
label: resourceName,
96112
type: "array",
97113
}
@@ -108,7 +124,7 @@ const dynamicValueOptions = computed(() => {
108124
const repeaterContext = props.block?.repeaterDataItem
109125
if (!isObjectEmpty(repeaterContext)) {
110126
const repeaterOptions = Object.keys(repeaterContext!).map((key) => ({
111-
value: formatValue(`dataItem.${key}`),
127+
value: `dataItem.${key}`,
112128
label: `dataItem.${key}`,
113129
type: typeof repeaterContext![key],
114130
}))

frontend/src/components/PropsEditor.vue

Lines changed: 27 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -6,44 +6,19 @@
66
<div v-else class="mt-3 flex flex-col gap-3">
77
<div v-for="(config, propName) in componentProps" :key="propName" class="group flex w-full items-center">
88
<DynamicValueSelector
9-
v-if="propName === 'modelValue'"
9+
v-if="!isTestingComponent"
1010
:block="block"
11-
@update:modelValue="(value) => bindVariable(propName, value)"
11+
@update:modelValue="(value, bindVariable) => setDynamicValue(propName, value, bindVariable)"
1212
:class="{ 'mt-1 self-start': isCodeField(config.inputType) }"
13-
:formatValuesAsTemplate="false"
14-
>
15-
<template #target="{ togglePopover }">
16-
<IconButton
17-
:icon="isVariableBound(config.modelValue) ? Link2Off : Link2"
18-
:label="isVariableBound(config.modelValue) ? 'Disable sync with variable' : 'Sync with variable'"
19-
placement="bottom"
20-
class="mr-1"
21-
@click="
22-
() => {
23-
if (isVariableBound(config.modelValue)) {
24-
unbindVariable(propName)
25-
} else {
26-
togglePopover()
27-
}
28-
}
29-
"
30-
/>
31-
</template>
32-
</DynamicValueSelector>
33-
34-
<DynamicValueSelector
35-
v-else-if="!isTestingComponent"
36-
:block="block"
37-
:class="{ 'mt-1 self-start': isCodeField(config.inputType) }"
38-
@update:modelValue="(value) => props.block?.setProp(propName, value)"
13+
:isVariableBound="isVariableBound(config.modelValue)"
3914
/>
4015

4116
<Code
4217
v-if="config.inputType === 'html'"
4318
:label="propName"
4419
language="html"
45-
:modelValue="config.modelValue"
46-
@update:modelValue="(newValue) => props.block?.setProp(propName, newValue)"
20+
:modelValue="getFormattedValue(propName)"
21+
@update:modelValue="(newValue) => handlePropUpdate(propName, newValue)"
4722
:required="config.required"
4823
:completions="(context: CompletionContext) => getCompletions(context, block?.getCompletions())"
4924
:showLineNumbers="false"
@@ -62,30 +37,21 @@
6237
v-else-if="config.inputType === 'code'"
6338
:label="propName"
6439
language="javascript"
65-
:modelValue="config.modelValue"
66-
@update:modelValue="(newValue) => props.block?.setProp(propName, newValue)"
40+
:modelValue="getFormattedValue(propName)"
41+
@update:modelValue="(newValue) => handlePropUpdate(propName, newValue)"
6742
:required="config.required"
6843
:completions="(context: CompletionContext) => getCompletions(context, block?.getCompletions())"
6944
:showLineNumbers="false"
7045
class="overflow-hidden"
7146
/>
7247
<InlineInput
73-
v-else-if="propName !== 'modelValue'"
74-
:label="propName"
75-
:type="config.inputType"
76-
:options="config.options"
77-
:required="config.required"
78-
:modelValue="config.modelValue"
79-
@update:modelValue="(newValue) => props.block?.setProp(propName, newValue)"
80-
class="flex-1"
81-
/>
82-
<InlineInput
83-
v-else-if="propName === 'modelValue'"
48+
v-else
8449
:label="propName"
8550
:type="config.inputType"
8651
:options="config.options"
8752
:required="config.required"
88-
v-model="boundValue"
53+
:modelValue="getFormattedValue(propName)"
54+
@update:modelValue="(newValue) => handlePropUpdate(propName, newValue)"
8955
class="flex-1"
9056
/>
9157
</div>
@@ -99,9 +65,6 @@ import Block from "@/utils/block"
9965
10066
import InlineInput from "@/components/InlineInput.vue"
10167
import { isObjectEmpty } from "@/utils/helpers"
102-
import IconButton from "@/components/IconButton.vue"
103-
import Link2 from "~icons/lucide/link-2"
104-
import Link2Off from "~icons/lucide/link-2-off"
10568
import Code from "@/components/Code.vue"
10669
import { useStudioCompletions } from "@/utils/useStudioCompletions"
10770
import type { CompletionContext } from "@codemirror/autocomplete"
@@ -201,29 +164,27 @@ const isCodeField = (inputType: string) => {
201164
return ["code", "html"].includes(inputType)
202165
}
203166
204-
// variable binding
205-
const boundValue = computed({
206-
get() {
207-
const modelValue = props.block?.componentProps.modelValue
208-
if (modelValue?.$type === "variable") {
209-
return `{{ ${modelValue.name} }}`
210-
}
211-
return modelValue
212-
},
213-
set(newValue) {
214-
props.block?.setProp("modelValue", newValue)
215-
},
216-
})
167+
function setDynamicValue(propName: string, varName: string, bindVariable: boolean) {
168+
if (bindVariable) {
169+
props.block?.setProp(propName, { $type: "variable", name: varName })
170+
} else {
171+
props.block?.setProp(propName, `{{ ${varName} }}`)
172+
}
173+
}
217174
218-
const isVariableBound = (value: any) => {
219-
return value?.$type === "variable" ? value.name : null
175+
const getFormattedValue = (propName: string) => {
176+
const value = props.block?.componentProps[propName]
177+
if (value?.$type === "variable") {
178+
return `{{ ${value.name} }}`
179+
}
180+
return value
220181
}
221182
222-
const bindVariable = (propName: string, varName: string) => {
223-
props.block?.setProp(propName, { $type: "variable", name: varName })
183+
const handlePropUpdate = (propName: string, newValue: any) => {
184+
props.block?.setProp(propName, newValue)
224185
}
225186
226-
const unbindVariable = (propName: string) => {
227-
props.block?.setProp(propName, "")
187+
const isVariableBound = (value: any) => {
188+
return value?.$type === "variable" ? value.name : null
228189
}
229190
</script>

0 commit comments

Comments
 (0)