diff --git a/examples/stac_gallery/assets/json/dynamic_view_example.json b/examples/stac_gallery/assets/json/dynamic_view_example.json index fff82635..b49bedc4 100644 --- a/examples/stac_gallery/assets/json/dynamic_view_example.json +++ b/examples/stac_gallery/assets/json/dynamic_view_example.json @@ -13,6 +13,28 @@ "url": "https://dummyjson.com/users/1", "method": "get" }, + "loaderWidget": { + "type": "center", + "child": { + "type": "column", + "children": [ + { + "type": "text", + "data": "Loading..." + }, + { + "type": "circularProgressIndicator" + } + ] + } + }, + "errorWidget": { + "type": "center", + "child": { + "type": "text", + "data": "Error fetching user profile" + } + }, "template": { "type": "column", "children": [ diff --git a/packages/stac/lib/src/parsers/widgets/stac_dynamic_view/stac_dynamic_view.dart b/packages/stac/lib/src/parsers/widgets/stac_dynamic_view/stac_dynamic_view.dart index b595aa65..0359a8dd 100644 --- a/packages/stac/lib/src/parsers/widgets/stac_dynamic_view/stac_dynamic_view.dart +++ b/packages/stac/lib/src/parsers/widgets/stac_dynamic_view/stac_dynamic_view.dart @@ -1,5 +1,5 @@ import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:stac/src/parsers/actions/stac_network_request/stac_network_request.dart'; +import 'package:stac/stac.dart'; export 'stac_dynamic_view_parser.dart'; @@ -15,6 +15,8 @@ abstract class StacDynamicView with _$StacDynamicView { @Default('') String targetPath, required Map template, @Default('') String resultTarget, + StacWidget? loaderWidget, + StacWidget? errorWidget, }) = _StacDynamicView; factory StacDynamicView.fromJson(Map json) => diff --git a/packages/stac/lib/src/parsers/widgets/stac_dynamic_view/stac_dynamic_view.freezed.dart b/packages/stac/lib/src/parsers/widgets/stac_dynamic_view/stac_dynamic_view.freezed.dart index b1e67458..c7b3470f 100644 --- a/packages/stac/lib/src/parsers/widgets/stac_dynamic_view/stac_dynamic_view.freezed.dart +++ b/packages/stac/lib/src/parsers/widgets/stac_dynamic_view/stac_dynamic_view.freezed.dart @@ -19,6 +19,8 @@ mixin _$StacDynamicView { String get targetPath; Map get template; String get resultTarget; + StacWidget? get loaderWidget; + StacWidget? get errorWidget; /// Create a copy of StacDynamicView /// with the given fields replaced by the non-null parameter values. @@ -41,17 +43,27 @@ mixin _$StacDynamicView { other.targetPath == targetPath) && const DeepCollectionEquality().equals(other.template, template) && (identical(other.resultTarget, resultTarget) || - other.resultTarget == resultTarget)); + other.resultTarget == resultTarget) && + const DeepCollectionEquality() + .equals(other.loaderWidget, loaderWidget) && + const DeepCollectionEquality() + .equals(other.errorWidget, errorWidget)); } @JsonKey(includeFromJson: false, includeToJson: false) @override - int get hashCode => Object.hash(runtimeType, request, targetPath, - const DeepCollectionEquality().hash(template), resultTarget); + int get hashCode => Object.hash( + runtimeType, + request, + targetPath, + const DeepCollectionEquality().hash(template), + resultTarget, + const DeepCollectionEquality().hash(loaderWidget), + const DeepCollectionEquality().hash(errorWidget)); @override String toString() { - return 'StacDynamicView(request: $request, targetPath: $targetPath, template: $template, resultTarget: $resultTarget)'; + return 'StacDynamicView(request: $request, targetPath: $targetPath, template: $template, resultTarget: $resultTarget, loaderWidget: $loaderWidget, errorWidget: $errorWidget)'; } } @@ -65,7 +77,9 @@ abstract mixin class $StacDynamicViewCopyWith<$Res> { {StacNetworkRequest request, String targetPath, Map template, - String resultTarget}); + String resultTarget, + StacWidget? loaderWidget, + StacWidget? errorWidget}); $StacNetworkRequestCopyWith<$Res> get request; } @@ -87,6 +101,8 @@ class _$StacDynamicViewCopyWithImpl<$Res> Object? targetPath = null, Object? template = null, Object? resultTarget = null, + Object? loaderWidget = freezed, + Object? errorWidget = freezed, }) { return _then(_self.copyWith( request: null == request @@ -105,6 +121,14 @@ class _$StacDynamicViewCopyWithImpl<$Res> ? _self.resultTarget : resultTarget // ignore: cast_nullable_to_non_nullable as String, + loaderWidget: freezed == loaderWidget + ? _self.loaderWidget + : loaderWidget // ignore: cast_nullable_to_non_nullable + as StacWidget?, + errorWidget: freezed == errorWidget + ? _self.errorWidget + : errorWidget // ignore: cast_nullable_to_non_nullable + as StacWidget?, )); } @@ -126,8 +150,12 @@ class _StacDynamicView implements StacDynamicView { {required this.request, this.targetPath = '', required final Map template, - this.resultTarget = ''}) - : _template = template; + this.resultTarget = '', + final StacWidget? loaderWidget, + final StacWidget? errorWidget}) + : _template = template, + _loaderWidget = loaderWidget, + _errorWidget = errorWidget; factory _StacDynamicView.fromJson(Map json) => _$StacDynamicViewFromJson(json); @@ -147,6 +175,25 @@ class _StacDynamicView implements StacDynamicView { @override @JsonKey() final String resultTarget; + final StacWidget? _loaderWidget; + @override + StacWidget? get loaderWidget { + final value = _loaderWidget; + if (value == null) return null; + if (_loaderWidget is EqualUnmodifiableMapView) return _loaderWidget; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(value); + } + + final StacWidget? _errorWidget; + @override + StacWidget? get errorWidget { + final value = _errorWidget; + if (value == null) return null; + if (_errorWidget is EqualUnmodifiableMapView) return _errorWidget; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(value); + } /// Create a copy of StacDynamicView /// with the given fields replaced by the non-null parameter values. @@ -173,17 +220,27 @@ class _StacDynamicView implements StacDynamicView { other.targetPath == targetPath) && const DeepCollectionEquality().equals(other._template, _template) && (identical(other.resultTarget, resultTarget) || - other.resultTarget == resultTarget)); + other.resultTarget == resultTarget) && + const DeepCollectionEquality() + .equals(other._loaderWidget, _loaderWidget) && + const DeepCollectionEquality() + .equals(other._errorWidget, _errorWidget)); } @JsonKey(includeFromJson: false, includeToJson: false) @override - int get hashCode => Object.hash(runtimeType, request, targetPath, - const DeepCollectionEquality().hash(_template), resultTarget); + int get hashCode => Object.hash( + runtimeType, + request, + targetPath, + const DeepCollectionEquality().hash(_template), + resultTarget, + const DeepCollectionEquality().hash(_loaderWidget), + const DeepCollectionEquality().hash(_errorWidget)); @override String toString() { - return 'StacDynamicView(request: $request, targetPath: $targetPath, template: $template, resultTarget: $resultTarget)'; + return 'StacDynamicView(request: $request, targetPath: $targetPath, template: $template, resultTarget: $resultTarget, loaderWidget: $loaderWidget, errorWidget: $errorWidget)'; } } @@ -199,7 +256,9 @@ abstract mixin class _$StacDynamicViewCopyWith<$Res> {StacNetworkRequest request, String targetPath, Map template, - String resultTarget}); + String resultTarget, + StacWidget? loaderWidget, + StacWidget? errorWidget}); @override $StacNetworkRequestCopyWith<$Res> get request; @@ -222,6 +281,8 @@ class __$StacDynamicViewCopyWithImpl<$Res> Object? targetPath = null, Object? template = null, Object? resultTarget = null, + Object? loaderWidget = freezed, + Object? errorWidget = freezed, }) { return _then(_StacDynamicView( request: null == request @@ -240,6 +301,14 @@ class __$StacDynamicViewCopyWithImpl<$Res> ? _self.resultTarget : resultTarget // ignore: cast_nullable_to_non_nullable as String, + loaderWidget: freezed == loaderWidget + ? _self._loaderWidget + : loaderWidget // ignore: cast_nullable_to_non_nullable + as StacWidget?, + errorWidget: freezed == errorWidget + ? _self._errorWidget + : errorWidget // ignore: cast_nullable_to_non_nullable + as StacWidget?, )); } diff --git a/packages/stac/lib/src/parsers/widgets/stac_dynamic_view/stac_dynamic_view.g.dart b/packages/stac/lib/src/parsers/widgets/stac_dynamic_view/stac_dynamic_view.g.dart index 007898ea..9c5134e0 100644 --- a/packages/stac/lib/src/parsers/widgets/stac_dynamic_view/stac_dynamic_view.g.dart +++ b/packages/stac/lib/src/parsers/widgets/stac_dynamic_view/stac_dynamic_view.g.dart @@ -13,6 +13,8 @@ _StacDynamicView _$StacDynamicViewFromJson(Map json) => targetPath: json['targetPath'] as String? ?? '', template: json['template'] as Map, resultTarget: json['resultTarget'] as String? ?? '', + loaderWidget: json['loaderWidget'] as Map?, + errorWidget: json['errorWidget'] as Map?, ); Map _$StacDynamicViewToJson(_StacDynamicView instance) => @@ -21,4 +23,6 @@ Map _$StacDynamicViewToJson(_StacDynamicView instance) => 'targetPath': instance.targetPath, 'template': instance.template, 'resultTarget': instance.resultTarget, + 'loaderWidget': instance.loaderWidget, + 'errorWidget': instance.errorWidget, }; diff --git a/packages/stac/lib/src/parsers/widgets/stac_dynamic_view/stac_dynamic_view_parser.dart b/packages/stac/lib/src/parsers/widgets/stac_dynamic_view/stac_dynamic_view_parser.dart index 1bd2273a..0b78b454 100644 --- a/packages/stac/lib/src/parsers/widgets/stac_dynamic_view/stac_dynamic_view_parser.dart +++ b/packages/stac/lib/src/parsers/widgets/stac_dynamic_view/stac_dynamic_view_parser.dart @@ -26,10 +26,11 @@ class StacDynamicViewParser extends StacParser { future: _fetchData(context, model), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { - return const Center(child: CircularProgressIndicator()); + return Stac.fromJson(model.loaderWidget, context) ?? + const Center(child: CircularProgressIndicator()); } else if (snapshot.hasError) { Log.e(snapshot.error); - return const SizedBox(); + return Stac.fromJson(model.errorWidget, context) ?? const SizedBox(); } else if (snapshot.hasData) { final response = snapshot.data; if (response != null) { @@ -70,7 +71,8 @@ class StacDynamicViewParser extends StacParser { } } catch (e) { Log.e('Error parsing API response: $e'); - return SizedBox(); + return Stac.fromJson(model.errorWidget, context) ?? + const SizedBox(); } } return const SizedBox(); diff --git a/website/docs/widgets/dynamic_view.md b/website/docs/widgets/dynamic_view.md index 664943b1..11958674 100644 --- a/website/docs/widgets/dynamic_view.md +++ b/website/docs/widgets/dynamic_view.md @@ -8,18 +8,23 @@ The `dynamicView` widget allows you to fetch data from an API and render it usin - Fetch data from any REST API endpoint - Apply data to templates with placeholder syntax -- Extract nested data using dot notation +- Extract nested data using dot notation and array indexing - Handle both single objects and lists of data - Render lists of items using the itemTemplate feature +- Customize loading and error states +- Target specific data paths within complex API responses ## Properties -| Property | Type | Required | Description | -|-------------|---------------------|----------|------------------------------------------------------------| -| request | `StacNetworkRequest` | Yes | API request configuration (url, method, headers, etc.) | -| template | `Map` | Yes | Template to render with data from the API response | -| targetPath | `String` | No | Path to extract specific data from the API response | -| itemTemplate | `Map` | No | Template to render each item in a list of items from the API response | +| Property | Type | Required | Description | +|---------------|---------------------|----------|------------------------------------------------------------| +| request | `StacNetworkRequest` | Yes | API request configuration (url, method, headers, etc.) | +| template | `Map` | Yes | Template to render with data from the API response | +| targetPath | `String` | No | Path to extract specific data from the API response | +| resultTarget | `String` | No | Key name to use when applying data to the template | +| loaderWidget | `Map` | No | Custom widget to display while loading data | +| errorWidget | `Map` | No | Custom widget to display when an error occurs | +| itemTemplate | `Map` | No | Template to render each item in a list of items from the API response | ## Basic Usage @@ -39,11 +44,15 @@ The `dynamicView` widget allows you to fetch data from an API and render it usin ## Data Placeholders -Use double curly braces `{{placeholder}}` to insert data from the API response into your template. For nested data, use dot notation: `{{user.address.city}}`. +Use double curly braces `{{placeholder}}` to insert data from the API response into your template: + +- For nested data, use dot notation: `{{user.address.city}}` +- For array elements, use index notation: `{{users[0].name}}` or combined path: `{{items.0.title}}` +- For array elements within objects, use combined notation: `{{data.users[2].profile.name}}` ## Examples -### User Profile Example +### User Profile Example with Loading and Error States ```json { @@ -52,22 +61,74 @@ Use double curly braces `{{placeholder}}` to insert data from the API response i "url": "https://dummyjson.com/users/1", "method": "get" }, + "loaderWidget": { + "type": "center", + "child": { + "type": "column", + "children": [ + { + "type": "text", + "data": "Loading..." + }, + { + "type": "circularProgressIndicator" + } + ] + } + }, + "errorWidget": { + "type": "center", + "child": { + "type": "text", + "data": "Error fetching user profile" + } + }, "template": { "type": "column", "children": [ { - "type": "image", - "src": "{{image}}", - "width": 100, - "height": 100 - }, - { - "type": "text", - "data": "{{firstName}} {{lastName}}" - }, - { - "type": "text", - "data": "Email: {{email}}" + "type": "container", + "padding": 16, + "child": { + "type": "column", + "crossAxisAlignment": "start", + "children": [ + { + "type": "image", + "src": "{{image}}", + "width": 100, + "height": 100 + }, + { + "type": "text", + "style": { + "fontSize": 24, + "fontWeight": "w700" + }, + "data": "{{firstName}} {{lastName}}" + }, + { + "type": "sizedBox", + "height": 8 + }, + { + "type": "text", + "style": { + "fontSize": 16, + "color": "#666666" + }, + "data": "Email: {{email}}" + }, + { + "type": "text", + "style": { + "fontSize": 16, + "color": "#666666" + }, + "data": "Phone: {{phone}}" + } + ] + } } ] } @@ -76,7 +137,6 @@ Use double curly braces `{{placeholder}}` to insert data from the API response i ### List Example with itemTemplate - When the API returns a list of items, use the `itemTemplate` property to define how each item should be rendered: ```json @@ -123,7 +183,45 @@ Use the `targetPath` property to extract specific data from complex API response }, "targetPath": "response.data.items", "template": { - // Template definition + "type": "column", + "children": [ + { + "type": "text", + "data": "Items loaded: {{length}}" + } + ] + } +} +``` + +### Using resultTarget + +The `resultTarget` property allows you to specify a key name to use when applying data to the template. This is useful when you want to reference the data with a specific name in your template: + +```json +{ + "type": "dynamicView", + "request": { + "url": "https://api.example.com/products", + "method": "get" + }, + "targetPath": "data.featured", + "resultTarget": "product", + "template": { + "type": "card", + "child": { + "type": "column", + "children": [ + { + "type": "text", + "data": "{{product.name}}" + }, + { + "type": "text", + "data": "Price: ${{product.price}}" + } + ] + } } } ``` @@ -144,7 +242,27 @@ Add custom headers to your API requests: } }, "template": { - // Template definition + "type": "text", + "data": "Data loaded successfully!" + } +} +``` + +### Array Indexing in targetPath + +You can access specific array elements in the targetPath: + +```json +{ + "type": "dynamicView", + "request": { + "url": "https://api.example.com/posts", + "method": "get" + }, + "targetPath": "data.posts[0]", + "template": { + "type": "text", + "data": "Featured Post: {{title}}" } } ``` @@ -153,11 +271,13 @@ Add custom headers to your API requests: 1. Use `targetPath` to extract only the data you need from complex API responses 2. For list data, always use the `itemTemplate` property to define how each item should be rendered -3. Keep templates modular and reusable when possible -4. Use appropriate error handling in your UI design for cases when the API request fails +3. Provide custom `loaderWidget` and `errorWidget` for better user experience +4. Use `resultTarget` when you need to reference the data with a specific name in your template +5. Keep templates modular and reusable when possible ## Limitations - API endpoints must return JSON data - For very large datasets, consider pagination or limiting the number of items to avoid performance issues -- Complex data transformations may require custom code outside of the template system \ No newline at end of file +- Complex data transformations may require custom code outside of the template system +- Nested array access in placeholder syntax is limited to the formats shown in the examples \ No newline at end of file