Can you use a backdraftjs watchable to make a component completely re-render?

32 Views Asked by At

This is a contrived example but it is similar to real-life situations where, for example, you might have a list of links built from data that you are AJAXing in from a server.

import {Component, e, render} from './node_modules/bd-core/lib.js';
            
// a list of strings that for alert when you click them
class AlertLinkList extends Component.withWatchables('data') {
    handleClick(event){
        alert(event.target.innerHTML);
    }
    
    bdElements() {
        return e.div(
            {
                // bdReflect: ?????
            },
            ['yes', 'no', 'maybe'].map(
                txt => e.div({bdAdvise: {click: 'handleClick'}}, txt)
            )
        )
    }
}

var linkList = render(AlertLinkList, {}, document.body);

// I would like to change the strings but this (obviously) does nothing
linkList.data = ['soup', 'nuts', 'fish', 'dessert'];

I can't think of a straightforward way to solve this.

bdReflect only works on writable DOM attributes, I think, so for example I could use it to replace the innerHTML of the component but then I think I lose the bdAdvise assignments on the links (and it also seems kinda kludgey).

Any ideas?

2

There are 2 best solutions below

1
On

OK here's one pattern that works for this...

  • get rid of the watchables in AlertLinkList
  • instead, use kwargs to populate the list
  • wrap the list in another component that simply re-renders the list with new content whenever the content changes (e.g. after fetching new content from the server)
// a list of strings that  alert when you click them
class AlertLinkList extends Component {
    handleClick(event){
        alert(event.target.innerHTML);
    }
    
    bdElements() {
        return e.div(
            this.kwargs.items.map(
                txt => e.div({bdAdvise: {click: 'handleClick'}}, txt)
            )
        )
    }
}

// a wrapper that provides/retrieves data for AlertLinkList
class LinkListWrapper extends Component {
    bdElements() {
        return e.div(
            {},
            e.a(
                {bdAdvise: {click: 'updateList'}},
                'Click to Update List',
            ),
            e.div({bdAttach: 'listGoesHere'}),
        );
    }

    updateList(event) {
        // the data below would have been retrieved from the server...
        const resultRetrievedFromServer = ['soup', 'nuts', 'fish', 'dessert'];
        this.renderList(resultRetrievedFromServer)
    }

    renderList(items) {
        render(AlertLinkList, {items}, this.listGoesHere, 'only')
    }

    postRender() {
        const initialData = ['yes', 'no', 'maybe']
        this.renderList(initialData);
    }
}

var linkList = render(LinkListWrapper, {}, document.body);

The only issue I see here is that it may be suboptimal to re-render the entire wrapped component if only one small part of the data changed, though I suppose you could design around that.

1
On

Let's begin solving this problem by describing the public interface of AlertLinkList:

  1. A component that contains a homogeneous list of children.
  2. The state of each child is initialized by a pair of [text, url].
  3. The list is mutated en masse.

Given this, your start is almost perfect. Here it is a again with a few minor modifications:

class AlertLinkList extends Component.withWatchables('data') {
    handleClick(event) {
        // do something when one of the children is clicked
    }

    bdElements() {
        return e.div({}, this.data && this.data.map(item => e(AlertLink, { data: item })));
    }

    onMutateData(newValue) {
        if (this.rendered) {
            this.delChildren();
            newValue && newValue.forEach(item => this.insChild(AlertLink, { data: item }));
        }
    }
}

See https://backdraftjs.org/tutorial.html#bd-tutorial.watchableProperties for an explanation of onMutateData.

Next we need to define the AlertLink component type; this is trivial:

class AlertLink extends Component {
    bdElements() {
        return e.a({
            href: this.kwargs.data[1],
            bdAdvise: { click: e => this.parent.handleClick(e) }
        }, this.kwargs.data[0]);
    }
}

The outline above will solve your problem. I've written the pen https://codepen.io/rcgill/pen/ExWrLbg to demonstrate.

You can also solve the problem with the backdraft Collection component https://backdraftjs.org/docs.html#bd-core.classes.Collection

I've written a pen https://codepen.io/rcgill/pen/WNpmeyx to demonstrate.

Lastly, if you're interested in writing the fewest lines of code possible and want a fairly immutable design, you don't have to factor out a child type. Here's a pen to demonstrate that: https://codepen.io/rcgill/pen/bGqZGgW

Which is best!?!? Well, it depends on your aims.

The first solution is simple and general and the children can be wrangled to do whatever you want them to do.

The second solution is very terse and includes a lot of additional capabilities not demonstrated. For example, with the backdraft Collection component

  • mutating the collection does not destroy/create new children, but rather alters the state of existing children. This is much more efficient and useful when implementing things like large grids.

  • you can mutate an individual elements in the collection

The third solution is very terse and very fixed. But sometimes that is all you need.