Data will always come from outside of application asynchronously. So we are always plagued by way of loading data, and making its view. If you don't care about lag of data load, your interface easily stack.
Best practice: Data injection
The best practice for this problem is "load data then make its view" pattern. (called as "Data injection" pattern)
It is best to implement controller class to load remote data, that makes its factorized view (Factorize pattern) after loaded data.
In other word, this is a kind of GoF Facade pattern, or minimized MVC structure. Usually, this controller represents reusable package.
If factorized view classes need to load remote data, the request should be delegated to the controller class. (Delegate pattern)
This will make complexity aggregated in the controller.
Example
Here is a sample remote data same as craftkit best practice - How to manage sub views for array data.
https://craftkit.dev/craftkit-playground/sampledata/simple_products.json
[
{ name: 'Apple', price: 10 },
{ name: 'Orange', price: 13 },
{ name: 'Strawberry', price: 20 },
{ name: 'Pear', price: 9 }
]
Here is a sample implementation to load and show remote product list.
class ProductListController extends Craft.UI.View {
constructor(options){
super(options);
this.data = { products:[] };
this.views = { current:null };
}
viewDidLoad(callback){
this.loadData();
this.renderReloadBtn();
if( callback ){ callback(); }
}
loadData(){
this.renderLoading();
let xhr = new XMLHttpRequest();
xhr.onload = (e) => {
this.data.products = JSON.parse(xhr.response);
this.renderProductList();
};
xhr.open('GET','https://craftkit.dev/craftkit-playground/sampledata/simple_products.json');
xhr.send();
}
renderLoading(){
let view = new Loading();
this.replaceView({id:'list',component:view});
this.views.current?.unloadView(); // for chrome
this.views.current = view;
}
renderProductList(){
let view = new ProductList({delegate:this});
this.replaceView({id:'list',component:view});
this.views.current?.unloadView(); // for chrome
this.views.current = view;
}
renderReloadBtn(){
this.appendView({
id: 'btn',
component: new ReloadBtn({delegate:this})
});
}
style(componentId){
return `
#btn { background-color:#eee; }
`;
}
template(componentId){
return `
<div id="root" class="root">
<div id="list"></div>
<div id="btn"></div>
</div>
`;
}
}
class Loading extends Craft.UI.View {
template(componentId){
return `
<div>Loading...</div>
`;
}
}
class ReloadBtn extends Craft.UI.View {
constructor(options){
super(options);
this.delegate = options.delegate;
}
reloadData(){
this.delegate.loadData();
}
style(componentId){
return `
.root { cursor:pointer; }
`;
}
template(componentId){
return `
<div id="root" class="root"
onclick="${componentId}.reloadData();">
Reload data
</div>
`;
}
}
class ProductList extends Craft.UI.View {
constructor(options){
super(options);
this.delegate = options.delegate;
this.views = { products:[] }
}
viewDidLoad(callback){
this.delegate.data.products.forEach( p => {
let view = new Product(p);
this.appendView(view);
this.views.products.push(view);
});
}
}
class Product extends Craft.UI.View {
constructor(options){
super(options);
this.data = options;
}
template(componentId){
return `
<div id="root" class="root">
${this.data.name} : ${this.data.price}
</div>
`;
}
}
Sometimes, controller is represented as Page object to be shown in navigation stack. Sometimes, controller is represented as micro-fronend UI component.
NOTE
Above examples are runnable on playground.
var view = new ProductListController();
view.loadView();
Craft.Core.Context.getRootViewController().appendSubView(view);
🛺 Try CraftKit Playground:
https://github.com/craftkit/craftkit-playground
Top comments (0)