For fine control of the rendering process, Beagle also lets you manage when to render a new tree to your Beagle View. You can have a component that, when clicked, changes the current Beagle Tree, so a new component is added or a property is modified.
A common scenario is when you need to create an action that modifies the current tree. Take for example the action addChildren
, when executed, it must get the current tree and add the given children to the component with the given id
. To do it, we must have a way to get the current tree, modify it and tell the Beagle View to render it again.
A renderization can be one of two processes: a full renderization or a partial renderization. The first runs all steps of the renderization (1 to 8 in this list). The second executes only the view snapshot and the steps after it (9 to 18 in this list).
Each Beagle View has its renderer, to get access to it, you must call beagleView.getRenderer()
.
See examples of how to get the renderer:
BeagleView
can be obtained through the property viewRef
of the BeagleRemoteView:
import React, { FC, useRef, useEffect, MutableRefObject } from 'react'
import { BeagleRemoteView } from '@zup-it/beagle-react'
import { BeagleView } from '@zup-it/beagle-web'
const Home: FC = () => {
const beagleView = useRef() as MutableRefObject<BeagleView | undefined>
useEffect(() => {
if (beagleView.current) {
const renderer = beagleView.current.getRenderer()
}
}, [])
return (
<BeagleRemoteView path="/home" viewRef={beagleView} />
)
}
onCreateBeagleView
of the beagle-remote-view
component:import { Component } from '@angular/core'
import { LoadParams, BeagleView } from '@zup-it/beagle-web'
@Component({
selector: 'home',
template: '',
})
export class Home {
loadParams: LoadParams
private beagleView: BeagleView
constructor() {
this.loadParams = { path: '/home' }
}
onCreateBeagleView(beagleView: BeagleView) {
this.beagleView = beagleView
}
getRenderer() {
return this.beagleView && this.beagleView.getRenderer()
}
}
onCreateView
of the BeagleWidget
:class Ref<T> {
T current;
}
class Home extends StatelessWidget {
Home({Key key}) : super(key: key);
// holds a reference to the BeagleView in case you need direct access to it.
final _beagleViewRef = Ref<BeagleView>();
@override
Widget build(BuildContext context) {
return MaterialApp(
home: BeagleWidget(
screenRequest: BeagleScreenRequest('/home'),
onCreateView: (view) {
_beagleViewRef.current = view;
}
),
);
}
),
When inside a component rendered by Beagle, you can use the ViewContentManager to get the BeagleView and obtain access to the renderer.
When inside an action handler (custom actions), the Beagle View is provided via parameter, which can be used to get the renderer. See the example below:
const MyCustomActionHandler: ActionHandler<MyCustomAction> = ({ action, beagleView }) => {
const renderer = beagleView.getRenderer()
// ...
}
final ActionHandler myAction = ({action, view, element, context}) {
final renderer = view.getRenderer();
// ...
};
The renderer API has two main functions: doFullRender
and doPartialRender
.
doFullRender
: renders the tree passed as parameter by running all rendering steps over it. Full renders must be done every time new nodes are created.doPartialRender
: it only runs the view snapshot and the steps after that. Partial renders should be used to modify existent nodes.The tree received by doFullRender
is of type BeagleUIElement
, its nodes might have ids or not. The tree received by doPartialRender
is of type
IdentifiableBeagleUIElement
, it must have ids for every node. You can never pass a tree to doPartialRender
with missing ids.
Besides the type of the tree, there is no difference to the way we call doFullRender
and doPartialRender
. They both accept the following parameters:
anchor
. How the attachment is done is defined by the third parameter.replaceComponent
. There are four different modes:replaceComponent
: replaces the node pointed by anchor
.replace
: replaces the children of the node pointed by anchor
.prepend
: adds to the children of the node pointed by anchor
. The new nodes are added before the existing nodes.append
: adds to the children of the node pointed by anchor
. The new nodes are added after the existing nodes.There’s a third additional method used to efficiently render lists based on a template, it’s called doTemplateRender
.
The doTemplateRender
renders according to a template manager and a matrix of contexts.
Each line in the matrix of contexts represents an iteration and each column represents the value
of a template variable. For instance, imagine a template with the variables @{name}
, @{sex}
and @{address}
. Now suppose we want to render three different entries with this template.
Here’s a context matrix that could be used for this example:
[
[{ id: 'name', value: 'John' }, { id: 'sex', value: 'M' }, { id: 'address', value: { street: '42 Avenue', number: '256' } }],
[{ id: 'name', value: 'Sue' }, { id: 'sex', value: 'F' }, { id: 'address', value: { street: 'St Monica St', number: '85' } }],
[{ id: 'name', value: 'Paul' }, { id: 'sex', value: 'M' }, { id: 'address', value: { street: 'Bv Kennedy', number: '877' } }],
]
Note that the parameter contexts
adds to the context hierarchy that is already present in the
tree, it doesn’t replace it, i.e. you can still use the contexts declared in the current tree.
For each line of the context matrix, a template is chosen from the template manager according to
case
, which is a Beagle expression that resolves to a boolean. case
is resolved using the entire
context of the current tree plus the contexts passed in the parameter contexts
corresponding to
the current iteration. If no template meets the condition the default template is used. If there’s
no default template, the iteration is skipped.
After processing all items, the resulting tree is attached to the current tree at the node with
id anchor
(passed as parameter).
The component manager is an optional parameter and is used to modify the resulting component. This can be very useful for managing ids, for instance. The component manager is a function that receives the component generated and the index of the current iteration, returning the modified component.
Parameters
/* example 1: renders a container with an empty list */
beagleView.getRenderer().doFullRender({
_beagleComponent_: 'beagle:container',
children: [
_beagleComponent_: 'custom:list',
id: 'list',
],
})
/* example 2: adds a property to the root of the currently rendered tree */
const current = beagleView.getTree()
current.newProperty = 'new'
beagleView.getRenderer().doPartialRender(current)
/* example 3: adds an element to the "custom:list" inside the container */
const item = {
_beagleComponent_: 'beagle:container',
children: [
{ _beagleComponent_: 'beagle:text', text: 'Client name: Jasnah Kholin' },
{ _beagleComponent_: 'beagle:text', text: 'Client age: 30' }
]
}
// You should always do full renders when creating new nodes
beagleView.getRenderer().doFullRender(item, 'list', { mode: 'append' })
renderer.doFullRender(tree, componentId, 'replaceComponent')
is called
in the onInit
of a component with id componentId
, Beagle will remove a component of the tree before it even gets the chance to be fully rendered.We should only manually call the APIs for rendering when absolutely necessary. This is important because once we call these APIs from within our components, we basically couple them to Beagle, which is not a good thing, since they now need Beagle to work properly.
Considering the default components provided by Beagle, the ListView and the LazyComponent are good examples of components that need to access these APIs, since they need to control the Beagle
rendering process by themselves. If your component really needs this “super-power”, it can be done via the ViewContentManager
.
The ViewContentManager provides a way to access the Beagle View and the node in the current Beagle tree that gave origin to the rendered component. With this you can access the renderer and call re-renders for this specific component.
See below examples of how to access the ViewContentManager
for a component that lazily load another view and renders it as its children.
The ViewContentManager
can be accessed inside a React component if this component implements the BeagleComponent interface.
import React, { FC, useEffect } from 'react'
import { BeagleComponent } from '@zup-it/beagle-react'
interface LazyInterface extends BeagleComponent {
url: string,
}
const LazyComponent: FC<LazyInterface> = ({ url, viewContentManager, children }) => {
async function loadUrl() {
/* Once the component implements the BeagleComponent interface, we have access to the
viewContentManager, but if the component was not instantiated by Beagle, the viewContentManager
will be undefined. We must verify it before continuing. */
if (!viewContentManager) {
return console.error('The LazyComponent cannot be instantiated outside a Beagle context.')
}
const response = await fetch(url)
const jsonView: string = await response.json()
const elementId = viewContentManager.getElementId()
viewContentManager.getBeagleView().getRenderer().doFullRenderer(jsonView, elementId, 'replace')
}
useEffect(loadUrl, [])
return children?.length ? children : <p>Loading...</p>
}
In Angular, if you need access to the ViewContentManager, the component class must extend BeagleComponent
, then, you just need to reference this.viewContentManager
.
import { Component, Input, AfterViewInit } from '@angular/core'
import { BeagleComponent } from '@zup-it/beagle-angular'
@Component({
// ...
template: '<p *ngIf="isLoading">Loading...</p><ng-content></ng-content>',
})
export class ListView extends BeagleComponent implements AfterViewInit {
@Input() url: string
isLoading = true
private async loadUrl() {
/* Once the component implements the BeagleComponent interface, we have access to the
viewContentManager, but if the component was not instantiated by Beagle, the viewContentManager
will be undefined. We must verify it before continuing. */
if (!this.viewContentManager) {
return console.error('The LazyComponent cannot be instantiated outside a Beagle context.')
}
const response = await fetch(url)
const jsonView: string = await response.json()
const elementId = viewContentManager.getElementId()
this.viewContentManager.getBeagleView().getRenderer().doFullRenderer(jsonView, elementId, 'replace')
this.isLoading = false
}
ngAfterViewInit() {
this.loadUrl()
}
}
In Flutter, at least for now, we don’t have access to the view content manager. To achieve the same behavior, we must use the view that is passed as parameter to every component builder. See the example below:
import 'package:http/http.dart' as http;
import 'package:flutter/material.dart';
import 'package:beagle/beagle.dart';
import 'package:beagle/interface/beagle_view.dart';
import 'package:beagle/model/tree_update_mode.dart';
import 'package:after_layout/after_layout.dart';
class LazyComponent extends StatefulWidget {
const LazyComponent({
Key key,
this.url,
this.id,
this.view,
this.child,
}) : super(key: key);
final String url;
final String id;
final BeagleView view;
final Widget child;
@override
_LazyComponent createState() => _LazyComponent();
}
class _LazyComponent extends State<LazyComponent> with AfterLayoutMixin<LazyComponent> {
Future<void> _loadView async () {
final response = await http.get(Uri.parse(widget.url));
final jsonMap = jsonDecode(result.body);
final loadedView = BeagleUIElement(jsonMap);
widget.view.getRenderer().doFullRender(loadedView, widget.id, TreeUpdateMode.replace);
}
@override
void afterFirstLayout(BuildContext context) {
_loadView()
}
@override
Widget build(BuildContext context) {
return widget.child ?? Text('Loading...');
}
}
ComponentBuilder LazyComponentBuilder() {
return (element, children, view) {
return BeagleLazyComponent(
key: element.getKey(),
url: element.getAttributeValue('url'),
id: element.getId(),
view: view,
child: children.isEmpty ? null : children[0],
);
};
}
The AfterLayout mixin can be found here.
The ViewContentManager have the following properties/functions
Property | Type | Definition |
---|---|---|
getElement() | function | returns the node in the Beagle Tree responsible for the renderization of the component. |
getElementId() | function | shortcut to getElement().id |
getBeagleView() | function | returns the BeagleView responsible for the view containing the component. |
In case you need to update the current view with a tree that comes from the backend, you should use the method fetch
of the BeagleView
. It will internally use all the cache mechanisms of Beagle and also do the rendering part.
Was this page helpful?
Glad to hear it! Please tell us how we can improve.
Sorry to hear that. Please tell us how we can improve.