Skip to content

Commit 6d05ab5

Browse files
committed
External Component Node #1
1 parent e0daca4 commit 6d05ab5

File tree

7 files changed

+290
-211
lines changed

7 files changed

+290
-211
lines changed

lib/codelessly_sdk.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export 'src/model/publish_source.dart';
1919
export 'src/model/sdk_publish_model.dart';
2020
export 'src/transformers/transformers.dart';
2121
export 'src/ui/codelessly_widget.dart';
22+
export 'src/ui/codelessly_context.dart';
2223
export 'src/ui/codelessly_widget_controller.dart';
2324
export 'src/utils/extensions.dart';
2425
export 'src/utils/flutter_icons_data_map.dart';

lib/src/transformers/node_transformers/node_transformers.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export 'passive_canvas_transformer.dart';
55
export 'passive_checkbox_transformer.dart';
66
export 'passive_divider_transformer.dart';
77
export 'passive_dropdown_transformer.dart';
8+
export 'passive_external_component_transformer.dart';
89
export 'passive_embedded_video_transformer.dart';
910
export 'passive_expansion_tile_transformer.dart';
1011
export 'passive_floating_action_button_transformer.dart';
@@ -24,8 +25,8 @@ export 'passive_slider_transformer.dart';
2425
export 'passive_spacer_transformer.dart';
2526
export 'passive_stack_transformer.dart';
2627
export 'passive_switch_transformer.dart';
28+
export 'passive_tab_bar_transformer.dart';
2729
export 'passive_text_field_transformer.dart';
2830
export 'passive_text_transformer.dart';
2931
export 'passive_variance_transformer.dart';
3032
export 'passive_web_view_transformer.dart';
31-
export 'passive_tab_bar_transformer.dart';
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import 'package:codelessly_api/codelessly_api.dart';
2+
import 'package:flutter/material.dart';
3+
import 'package:provider/provider.dart';
4+
5+
import '../../../codelessly_sdk.dart';
6+
7+
class PassiveExternalComponentTransformer
8+
extends NodeWidgetTransformer<ExternalComponentNode> {
9+
PassiveExternalComponentTransformer(super.getNode, super.manager);
10+
11+
@override
12+
Widget buildWidget(
13+
ExternalComponentNode node,
14+
BuildContext context, [
15+
WidgetBuildSettings settings = const WidgetBuildSettings(),
16+
]) {
17+
return PassiveExternalComponentWidget(node: node, settings: settings);
18+
}
19+
}
20+
21+
class PassiveExternalComponentWidget extends StatelessWidget {
22+
final ExternalComponentNode node;
23+
final WidgetBuildSettings settings;
24+
25+
const PassiveExternalComponentWidget({
26+
super.key,
27+
required this.node,
28+
this.settings = const WidgetBuildSettings(),
29+
});
30+
31+
@override
32+
Widget build(BuildContext context) {
33+
final CodelesslyContext codelesslyContext =
34+
context.watch<CodelesslyContext>();
35+
final WidgetBuilder? builder =
36+
codelesslyContext.externalComponentBuilders[node.builderID];
37+
38+
return AdaptiveNodeBox(
39+
node: node,
40+
child: builder?.call(context) ?? const Placeholder(),
41+
);
42+
}
43+
}

lib/src/transformers/passive_transformer_manager.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ class PassiveNodeTransformerManager extends WidgetNodeTransformerManager {
5252
'listView': PassiveListViewTransformer(getNode, this),
5353
'pageView': PassivePageViewTransformer(getNode, this),
5454
'tabBar': PassiveTabBarTransformer(getNode, this),
55+
'external': PassiveExternalComponentTransformer(getNode, this),
5556
});
5657
}
5758

lib/src/ui/codelessly_context.dart

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
import 'dart:developer';
2+
3+
import 'package:codelessly_api/codelessly_api.dart';
4+
import 'package:collection/collection.dart';
5+
import 'package:equatable/equatable.dart';
6+
import 'package:flutter/material.dart';
7+
8+
import '../../codelessly_sdk.dart';
9+
10+
/// Holds data passed from the Codelessly instance down the widget tree where
11+
/// all of the [WidgetNodeTransformer]s have access to it.
12+
class CodelesslyContext with ChangeNotifier, EquatableMixin {
13+
/// A map of data that is passed to loaded layouts for nodes to replace their
14+
/// values with.
15+
Map<String, dynamic> _data;
16+
17+
/// A map of data that is passed to loaded layouts for nodes to replace their
18+
/// values with.
19+
Map<String, dynamic> get data => _data;
20+
21+
/// A map of data that is passed to loaded layouts for nodes to replace their
22+
/// values with.
23+
set data(Map<String, dynamic> value) {
24+
_data = value;
25+
notifyListeners();
26+
}
27+
28+
/// The passed ID of the layout to load.
29+
String? layoutID;
30+
31+
/// A map of functions that is passed to loaded layouts for nodes to call when
32+
/// they are triggered.
33+
Map<String, CodelesslyFunction> functions;
34+
35+
set setFunctions(Map<String, CodelesslyFunction> functions) {
36+
this.functions = functions;
37+
notifyListeners();
38+
}
39+
40+
/// A map of widget builders used to build dynamic widgets.
41+
Map<String, WidgetBuilder> externalComponentBuilders;
42+
43+
set setDynamicWidgetBuilders(Map<String, WidgetBuilder> externalComponentBuilders) {
44+
this.externalComponentBuilders = externalComponentBuilders;
45+
notifyListeners();
46+
}
47+
48+
/// A map that holds the current values of nodes that have internal values.
49+
final Map<String, ValueNotifier<List<ValueModel>>> nodeValues;
50+
51+
/// A map that holds the current state of all variables.
52+
/// The key is the variable's id.
53+
final Map<String, ValueNotifier<VariableData>> variables;
54+
55+
/// A map that holds the current state of all conditions.
56+
/// The key is the condition's id.
57+
final Map<String, BaseCondition> conditions;
58+
59+
/// Creates a [CodelesslyContext] with the given [data], [functions], and
60+
/// [nodeValues].
61+
CodelesslyContext({
62+
required Map<String, dynamic> data,
63+
required this.functions,
64+
required this.externalComponentBuilders,
65+
required this.nodeValues,
66+
required this.variables,
67+
required this.conditions,
68+
required this.layoutID,
69+
}) : _data = data;
70+
71+
/// Creates a [CodelesslyContext] with empty an empty map of each property.
72+
CodelesslyContext.empty({String? layoutID})
73+
: _data = {},
74+
functions = {},
75+
externalComponentBuilders = {},
76+
nodeValues = {},
77+
variables = {},
78+
conditions = {};
79+
80+
/// Returns a map of all of the [VariableData]s in [variables] mapped by their
81+
/// name.
82+
Map<String, VariableData> variableNamesMap() =>
83+
variables.map((key, value) => MapEntry(value.value.name, value.value));
84+
85+
/// Creates a copy of this [CodelesslyContext] with the given [data],
86+
/// [functions], and [nodeValues].
87+
CodelesslyContext copyWith({
88+
Map<String, dynamic>? data,
89+
Map<String, CodelesslyFunction>? functions,
90+
Map<String, WidgetBuilder>? dynamicWidgetBuilders,
91+
Map<String, ValueNotifier<List<ValueModel>>>? nodeValues,
92+
Map<String, ValueNotifier<VariableData>>? variables,
93+
Map<String, BaseCondition>? conditions,
94+
String? layoutID,
95+
bool forceLayoutID = false,
96+
}) {
97+
return CodelesslyContext(
98+
data: data ?? this.data,
99+
functions: functions ?? this.functions,
100+
externalComponentBuilders: dynamicWidgetBuilders ?? this.externalComponentBuilders,
101+
nodeValues: nodeValues ?? this.nodeValues,
102+
variables: variables ?? this.variables,
103+
layoutID: forceLayoutID ? layoutID : layoutID ?? this.layoutID,
104+
conditions: conditions ?? this.conditions,
105+
);
106+
}
107+
108+
/// Used for actions that are connected to one or more nodes.
109+
/// Ex. submit action is connected to a text field node to access its data to
110+
/// submit to the server.
111+
Future<void> handleActionConnections(
112+
ActionModel actionModel,
113+
Map<String, BaseNode> nodes,
114+
) async {
115+
switch (actionModel.type) {
116+
case ActionType.submit:
117+
final action = actionModel as MailchimpSubmitAction;
118+
final BaseNode? primaryField = nodes[action.primaryTextField];
119+
final BaseNode? firstNameField = nodes[action.firstNameField];
120+
final BaseNode? lastNameField = nodes[action.lastNameField];
121+
if (primaryField != null) {
122+
addToNodeValues(primaryField, [StringValue(name: 'inputValue')]);
123+
}
124+
if (firstNameField != null) {
125+
addToNodeValues(firstNameField, [StringValue(name: 'inputValue')]);
126+
}
127+
if (lastNameField != null) {
128+
addToNodeValues(lastNameField, [StringValue(name: 'inputValue')]);
129+
}
130+
break;
131+
case ActionType.setValue:
132+
final action = actionModel as SetValueAction;
133+
final SceneNode? connectedNode = nodes[action.nodeID] as SceneNode?;
134+
// Populate node values with node's values, not action's values.
135+
if (connectedNode != null) {
136+
addToNodeValues(
137+
connectedNode,
138+
connectedNode.propertyVariables
139+
.where((property) =>
140+
action.values.any((value) => property.name == value.name))
141+
.toList());
142+
}
143+
break;
144+
case ActionType.setVariant:
145+
final action = actionModel as SetVariantAction;
146+
final VarianceNode? connectedNode =
147+
nodes[action.nodeID] as VarianceNode?;
148+
// Populate node values with node's variant value, not action's variant
149+
// value.
150+
if (connectedNode != null) {
151+
addToNodeValues(connectedNode, [
152+
StringValue(
153+
name: 'currentVariantId',
154+
value: connectedNode.currentVariantId,
155+
)
156+
]);
157+
}
158+
break;
159+
case ActionType.setVariable:
160+
final action = actionModel as SetVariableAction;
161+
final VariableData variable = action.variable;
162+
variables[variable.id] = ValueNotifier(variable);
163+
default:
164+
}
165+
}
166+
167+
/// Add [values] to the [nodeValues] map corresponding to the [node].
168+
/// [values] refer to the local values of the node's properties that can be
169+
/// changed, for example, with set value action.
170+
void addToNodeValues(BaseNode node, List<ValueModel> values) {
171+
// Get current values for the node, if any.
172+
final List<ValueModel> currentValues = nodeValues[node.id]?.value ?? [];
173+
// New values.
174+
final List<ValueModel> newValues = [];
175+
// Filter out and populate new values.
176+
for (final ValueModel value in values) {
177+
if (!currentValues
178+
.any((currentValue) => currentValue.name == value.name)) {
179+
newValues.add(value);
180+
}
181+
}
182+
// Add new values to the node's values list.
183+
if (nodeValues[node.id] == null) {
184+
nodeValues[node.id] = ValueNotifier([...currentValues, ...newValues]);
185+
} else {
186+
nodeValues[node.id]!.value = [...currentValues, ...newValues];
187+
}
188+
}
189+
190+
/// Returns a reverse-lookup of the [VariableData] associated with a given
191+
/// [name].
192+
ValueNotifier<VariableData>? findVariableByName(String? name) =>
193+
variables.values
194+
.firstWhereOrNull((variable) => variable.value.name == name);
195+
196+
/// Allows to easily [value] of a variable with a given [name].
197+
/// Returns false if the variable does not exist.
198+
/// Returns true if the variable was updated successfully.
199+
bool updateVariable(String name, Object? value) {
200+
final ValueNotifier<VariableData>? variable = findVariableByName(name);
201+
if (variable == null) {
202+
log('[CodelesslyContext] Variable with name $name does not exist.');
203+
return false;
204+
}
205+
final String newValue = value == null ? '' : '$value';
206+
207+
// If the value is the same, then the underlying value notifier will not
208+
// notify listeners, so we need to return false.
209+
if (variable.value.value == newValue) {
210+
log('[CodelesslyContext] Variable with name $name already has the value $newValue.');
211+
return false;
212+
}
213+
variable.value = variable.value.copyWith(value: newValue);
214+
return true;
215+
}
216+
217+
/// Allows to easily get the [value] of a variable with a given [name].
218+
/// Returns null if the variable does not exist.
219+
/// If [R] is provided, the returned value will be cast to that type.
220+
R? getVariableValue<R extends Object>(String name) {
221+
final ValueNotifier<VariableData>? variable = findVariableByName(name);
222+
if (variable == null) {
223+
log('[CodelesslyContext] Variable with name $name does not exist.');
224+
return null;
225+
}
226+
return variable.value.getValue().typedValue<R>();
227+
}
228+
229+
@override
230+
List<Object?> get props => [layoutID, data, functions];
231+
}

0 commit comments

Comments
 (0)