How to redraw specific DOM asynchronously in Mithril

253 Views Asked by At

I have sub-components that are updated by injecting state from the parent component.

I need to populate the model using an asynchronous function when the value of the parent component changes.

And I want to draw a new subcomponent after the asynchronous operation is finished.

I checked the change of the parent component value in onbeforeupdate, executed the asynchronous function, and then executed the redraw function, but this gets stuck in an infinite loop.

...

async onbeforeupdate((vnode)) => {
  if (this.prev !== vnode.attrs.after) {
    // Update model data
    await asyncRequest();
    m.redraw();
  }
}

view() {
  return (...)
}

...

2

There are 2 best solutions below

0
Ian Wilson On

As far as I can tell onbeforeupdate does not seem to support async calling style, which makes sense because it would hold up rendering. The use-case for onbeforeupdate is when you have a table with 1000s of rows. In which case you'd want to perform that "diff" manually. Say by comparing items length to the last length or some other simple computation.

This sort of change detection should happen in your model when the parent model changes, trigger something that changes the child model. Then the child view will return a new subtree which will be rendered.

In this small example the list of items are passed directly from the parent to the child component. When the list of items increases, by pressing the load button, the updated list of items is passed to the child and the DOM is updated during the redraw. There is another button to toggle if the decision to take a diff in the view should be done manually or not.

You can see when the views are called in the console.

The second example is the more common/normal Mithril usage (with classes).

Manual Diff Decision Handling

<!doctype html>
<html>
    <body>
        <script src="https://unpkg.com/mithril/mithril.js"></script>

        <div id="app-container"></div>
        <script>
         let appContainerEl = document.getElementById('app-container');
         function asyncRequest() {
             return new Promise(function (resolve, reject) {
                 window.setTimeout(() => {
                     let res = [];
                     if (Math.random() < 0.5) {
                         res.push('' + (new Date().getTime() / 1000));
                         console.log('Found new items: '+ res[0]);
                     } else {
                         console.log('No new items.');
                     }
                     resolve(res);
                     // Otherwise reject()
                 }, 1000);
             });
         }
         class AppModel {
             /* Encapsulate our model. */
             constructor() {
                 this.child = {
                     items: [],
                     manualDiff: true,
                 };
             }
             async loadMoreChildItems() {
                 let values = await asyncRequest();
                 for (let i = 0, limit = values.length; i < limit; i += 1) {
                     this.child.items[this.child.items.length] = values[i];
                 }
             }
             getChildItems() {
                 return this.child.items;
             }
             toggleManualDiff() {
                 this.child.manualDiff = !this.child.manualDiff;
             }
             getManualDiffFlag() {
                 return this.child.manualDiff;
             }
         }
         function setupApp(model) {
             /* Set our app up in a namespace. */
             class App {
                 constructor(vnode) {
                     this.model = model;
                 }
                 view(vnode) {
                     console.log("Calling app view");
                     return m('div[id=app]', [
                         m(Child, {
                             manualDiff: this.model.getManualDiffFlag(),
                             items: this.model.getChildItems(),
                         }),
                         m('button[type=button]', {
                             onclick: (e) => {
                                 this.model.toggleManualDiff();
                             }
                         }, 'Toggle Manual Diff Flag'),
                         m('button[type=button]', {
                             onclick: (e) => {
                                 e.preventDefault();
                                 // Use promise returned by async function.
                                 this.model.loadMoreChildItems().then(function () {
                                     // Async call doesn't trigger redraw so do it manually.
                                     m.redraw();
                                 }, function (e) {
                                     // Handle reject() in asyncRequest.
                                     console.log('Item loading failed:' + e);
                                 });
                             }
                         }, 'Load Child Items')]);
                 }
             }
             class Child {
                 constructor(vnode) {
                     this.lastLength = vnode.attrs.items.length;
                 }
                 onbeforeupdate(vnode, old) {
                     if (vnode.attrs.manualDiff) {
                         // Only perform the diff if the list of items has grown.
                         // THIS ONLY WORKS FOR AN APPEND ONLY LIST AND SHOULD ONLY
                         // BE DONE WHEN DEALING WITH HUGE SUBTREES, LIKE 1000s OF
                         // TABLE ROWS.  THIS IS NOT SMART ENOUGH TO TELL IF THE
                         // ITEM CONTENT HAS CHANGED.
                         let changed = vnode.attrs.items.length > this.lastLength;
                         if (changed) {
                             this.lastLength = vnode.attrs.items.length;
                         }
                         console.log("changed=" + changed + (changed ? ", Peforming diff..." : ", Skipping diff..."));
                         return changed;
                     } else {
                         // Always take diff, default behaviour.
                         return true;
                     }
                 }
                 view(vnode) {
                     console.log("Calling child view");
                     // This will first will be an empty list because items is [].
                     // As more items are loaded mithril will take diffs and render the new items.
                     return m('.child', vnode.attrs.items.map(function (item) { return m('div', item); }));
                 }
             }
             // Mount our entire app at this element.
             m.mount(appContainerEl, App);
         }
         // Inject our model.
         setupApp(new AppModel());
        </script>
    </body>
</html>

Normal Usuage

<!doctype html>
<html>
    <body>
        <script src="https://unpkg.com/mithril/mithril.js"></script>

        <div id="app-container"></div>
        <script>
         let appContainerEl = document.getElementById('app-container');
         function asyncRequest() {
             return new Promise(function (resolve, reject) {
                 window.setTimeout(() => {
                     let res = [];
                     if (Math.random() < 0.5) {
                         res.push('' + (new Date().getTime() / 1000));
                         console.log('Found new items: '+ res[0]);
                     } else {
                         console.log('No new items.');
                     }
                     resolve(res);
                     // Otherwise reject()
                 }, 1000);
             });
         }
         class App {
             constructor(vnode) {
                 this.items = [];
             }
             async loadMoreChildItems() {
                 let values = await asyncRequest();
                 for (let i = 0, limit = values.length; i < limit; i += 1) {
                     this.items[this.items.length] = values[i];
                 }
             }
             view(vnode) {
                 console.log("Calling app view");
                 return m('div[id=app]', [
                     m(Child, {
                         items: this.items
                     }),
                     m('button[type=button]', {
                         onclick: (e) => {
                             e.preventDefault();
                             // Use promise returned by async function.
                             this.loadMoreChildItems().then(function () {
                                 // Async call doesn't trigger redraw so do it manually.
                                 m.redraw();
                             }, function (e) {
                                 // Handle reject() in asyncRequest.
                                 console.log('Item loading failed:' + e);
                             });
                         }
                     }, 'Load Child Items')]);
             }
         }
         class Child {
             view(vnode) {
                 console.log("Calling child view");
                 // This will first will be an empty list because items is [].
                 // As more items are loaded mithril will take diffs and render the new items.
                 return m('.child', vnode.attrs.items.map(function (item) { return m('div', item); }));
             }
         }
         // Mount our entire app at this element.
         m.mount(appContainerEl, App);
        </script>
    </body>
</html>
0
Stephan Hoyer On

It should work. Maybe something is wrong with updating this.prev