FFI to Material UI with Halogen - runtime errors

Hello, we are currently working on binding Material UI components to the Halogen components.
Basically, for each component we create a component in Halogen, handling initialization in Initialize action, and destruction in Finalize action, according to the component lifecycle.

So everything went smooth until we wanted to bind to data tables FFI (Material Design). We instantiate an MDCDataTable component from the root element referenced by a RefLabel and return it back from the FFI for later calls. And everything works just fine, animations, selection events, highlighting, etc… Until we change contents of a table.

The specification says to call layout() when we add or remove row checkboxes, so that the MDC code can reinitialize event handlers, destroy old ones and so on. Adding new rows is not a problem - when we modify a component state so that new rows are rendered, calling foreign layout() afterwards works just fine. All takes a bad turn, when we modify a state so that rows are removed, and then try to layout (even when we remove only rows added before). We get a runtime error

Uncaught Error: Checkbox component requires a .mdc-checkbox__native-control element (…)

It seems that it errors during layout, when it tries do destroy previously-registered row checkboxes. That is weird, because it does the same during finalize, and we never get errors during finalization. What is more weird, when we tried to debug the error, it seems that foreign code actually does find a component with no native-control element:

<div class="mdc-checkbox mdc-data-table__row-checkbox mdc-checkbox--upgraded" style></div>

But we never create such checkbox elements (no style, no children)! It looks as if Halogen removed part of the DOM referenced in a foreign code (all its children). After the error is raised, we cannot find such an element on page (it does not exist), so it looks like foreign code did not keep up with the changes.

Again, sorry for not giving any more context and any specific example, but maybe something would ring a bell. Perhaps it is a simple mistake, but I’m not familiar at all with Halogen internals.

1 Like

Are you using MDC Web Vanilla Components, or Foundations and Adapters?

Have you tried to mock-up the halogen-to-MDC boundary to make sure the component works the way you expect without halogen?

1 Like

Sorry, but I don’t understand the question / difference between the two. We wrap three FFI calls in our component .js counterpart:

"use strict";

const mdcDataTable = require('@material/data-table');

exports.initTable = element => () => {
    const dataTable = new mdcDataTable.MDCDataTable(element);

    dataTable.listen('MDCDataTable:rowSelectionChanged', data => {
        var eventData = { detail: data.detail };
        var event = new CustomEvent('customOnRowSelect', eventData);
        element.dispatchEvent(event);
      });

   (two more custom listeners here)

    return dataTable;
}

exports.layoutTable = dataTable => () => {
    dataTable.layout();
}

exports.destroyTable = dataTable => () => {
    dataTable.destroy();
}

We call initTable during component initialization, destroy during finalization, and layout after we modify a state so that a row set changes.

As I mentioned, everything works just fine (animations, checkboxes, custom events etc.) until we try to remove rendered rows during modify and then call layout.

I don’t understand the question / difference between the two

MDC Web Vanilla Components wraps MDC Foundations and Adapters specifically for the web. You can read about the differences here. Though from what you’ve shown me, you’re using Web Components.

we try to remove rendered rows during modify and then call layout.

Yeah, this is interesting. I don’t know Halogen well enough to be sure, but I suspect some optimization from the VDOM is at work.

Both halogen and the data table manage state by diffing the DOM, If halogen is updating old elements and the data table is expecting to manage fresh elements (so deleted elements are actually gone), you may get some unexpected results.

Though I’m not sure.

It would explain why removing everything (like during finalize) doesn’t create this issue, as the state between halogen and the data table would align.


One possible solution is to implement the Data Table adapter (from foundations and adapters) directly with Halogen, that way your adapter is using halogen’s VDOM and you don’t run into any synchronization issues.

Another solution is to bypass the VDOM for your data table. You should be able update the DOM under your table directly so long as you only give halogen the Root element and nothing more. This would remove any syncing issues again, but you run into some performance considerations as the Data Table will have to effectively do a full layout every render (unless you managing diffing the table yourself, in which case why re-invent the wheel, just write the adapter).

1 Like

Thanks. Yes, implementing the adapter is a possibility I considered, though I don’t have much experience in calling PureScript from JavaScript.

1 Like

Is there any good way to query Halogen components from foreign code?

Subscribe on requests from mui, but these requests should give you a callback

I too wanted to write a lib for MCW, there were two options

  1. Lib that adds new html tags but uses js lib internally - easiest option, but doesn’t work with hydration
  2. (The option you are using) js lib - should work, but I looked inside, it’s spaghetti, so decided to implement myself/port elm version, it’s not that hard

Here is a current version, only buttons stopped working, outlined input needs to be fixed - GitHub - srghma/purescript-halogen-material-components-web

I think @thomashoneyman would have an idea of what is idiomatic.


I don’t see what would stop you from hooking into Halogen’s query/event system and using that. This is the way Halogen hooks into DOM Events or Parent Component Events. I’m just speculating at this point though!

I managed to use halogen-subscriptions. I pass a listener to the foreign code, while the emitter is subscribed by the component. Foreign code sends queries to the component by passing argument and callback through notifying the listener. Looks a bit dirty, but it works. I wonder whether there is a more idiomatic way to do it. Perhaps I should study the halogen-subscriptions source code.

1 Like

For bindings to Halogen components in JavaScript, check out this conversation:

3 Likes

I did not know about this trick. That’s amazing.

@Swordlash, are your bindings to Material UI publicly visible or is there an example of what they look like that I can see?

Thanks!

Sadly, no. We are using it internally in my company.
But when they are finished, I will ask the supervisors for permission to release them open-source.

But the FFI to Material UI is pretty straightforward, I only had problems with DataTable, where I had to implement a whole Adapter from Halogen side. The rest (I now use textfields, select boxes, checkboxes) works out-of-the-box with js vanilla components.