Custom App Connect components

@JonL Just curious: Have you mesured the performance impact of having say about 100 such localized strings on a page? We have a multi-lingual Wappler project where we could use such a client-side formatter.

No noticeable impact to performance on a 2015 laptop. Everything is quite snappy. Haven’t tried on older computers though where memory and cpu might be a bottleneck if too many things are happening on the browser.

I will gladly accept contributions if you need to extend it. It’s not yet available in a git repository as I was waiting for the whole thing(UI hjson) to be implemented, but if you guys need it before that I can publish it.

1 Like

Excited to hear that. :star_struck:

Would be happy to contribute. :slight_smile:
But don’t have much bandwidth right now to try to implement this. So take your time with the repository.
Maybe few weeks down the line I’ll have some breathing time to replace existing logic.

1 Like

If you only use static text then you don’t have to do the $addBinding, that is only for expressions and will be evaluated each time any data changes (when dmx.requestUpdate() is called).

dmx.Attribute("i18n", "mounted", function (node, attr) {
    i18next.on('languageChanged', function update() {
        node.innerText = i18next.t(attr.value);
    });
    i18next.on('loaded', function update() {
        node.innerText = i18next.t(attr.value);
    });
});

You then use the attribute without expression:

<div>dmx-i18n: <span dmx-i18n="perks"></span></div>

That would probably benefit the performance.

1 Like

Interesting. Thanks @patrick. In my personal implementation I only need to evaluate static text, but as far as the extension goes I guess I would have to consider both.

Could I just do a quick if…else to add an $addBinding if the attr is an expression and assign the innerText to the static value if not? To benefit from the performance but leave the option if someone is calculating the value of the string to be translated dynamically.

What do you use to check if a value is an expression? I believe I’ve seen somewhere that you’ve done that but I can’t remember where exactly.

Edit: Other than the obvious if (attr.value[0] == "'" && attr.value[attr.value.length - 1] == "'") but I’m guessing this may miss some expressions like 'string'.uppercase()

It is difficult to tell if it is an expression or not, perks could be an expression variable or just the text perks. Normally we have all dynamic attributes, the ones starting with dmx-, have the value an expression. But we also have some cases where this is not always the case.

Here a bit more advanced example of the attribute usage for you:

dmx.Attribute("i18n-count", "mounted", function (node, attr) {
    var n = 0;
    var updateText = function() {
        node.innerText = i18next.t(attr.argument, { count: n });
    };

    i18next.on('languageChanged', updateText);
    i18next.on('loaded', updateText);

    this.$addBinding(attr.value, function (value) {
        n = value;
        updateText();
    });
});

Usage where items is the key and count is an expression:

<span dmx-i18n-count:items="count"></span>

A word after the ‘:’ will be used available as argument, we use this for example in the dmx-on:event and to name repeaters like dmx-repeat:name. You can also have modifiers on an attribute like dmx-on:event.stop.debounce:100="expr", which will be parsed into the following object:

"attr": {
  "name": "on",
  "argument": "event",
  "value": "expr",
  "modifiers": {
    "stop": true,
    "debounce": "100"
  }
}

The parts in the attribute name is case insensitive, they always become lower-case. Only the attribute value is case-sensitive.

2 Likes

@patrick, you read my mind because the next thing I had to tackle was interpolation, plurals(the example you gave) and other helpers.

You just tackled it yourself :slight_smile:

Thanks man!

So I could always go for something like this.

 <span dmx-i18n:static="perks"></span>
 <span dmx-i18n="'perks'"></span>
if (attr.argument == 'static') {
        i18next.on('languageChanged', function (lng) {
            node.innerText = i18next.t(attr.value);
        });
        i18next.on('initialized', function (loaded) {
            node.innerText = i18next.t(attr.value);
        });
    }
    else {
        this.$addBinding(attr.value, function (value) {
            i18next.on('languageChanged', function update() {
                node.innerText = i18next.t(value);
            });
            i18next.on('initialized', function update() {
                node.innerText = i18next.t(value);
            });
        });
    }

As I don’t know how the UI stuff will go will this compatible with letting the end user chose if he wants to add the normal attribute or the static version as an argument?

That is indeed a way to tackle the static/dynamic problem. It would be better to somehow place the i18next.on outside of the this.$addBinding, since it will bind a new event listener each time there is some data updated and that could cause some problems.

1 Like

Makes sense. Thanks again.

Does this look better?

The initialized listener is holding well and it’s unsuscribed at the end. And the dmx binding no longer binds a new event listener.

So far so good but I wanted to double check with you the addBinding added inside the event listener implementation.

dmx.Attribute("i18n", "mounted", function (node, attr) {
    i18next.on('initialized', function update() {
        if (attr.argument == 'static') {
            node.innerText = i18next.t(attr.value);
            i18next.on('languageChanged', function (lng) {
                node.innerText = i18next.t(attr.value);
            });
        }
        else {
            dmx.app.$addBinding(attr.value, function (value) {
                node.innerText = i18next.t(value);
            })
            i18next.on('languageChanged', function update(lng) {
                dmx.app.$addBinding(attr.value, function (value) {
                    node.innerText = i18next.t(value);
                })
            })
        }
        i18next.off('initialized', update);
    });
})

That looks a lot better, prevents a lot of memory leaks. It would be preferable to also have the $addBinding outside of the event listener, you also want to have that only executed ones. But that event is probably not triggered that often.

1 Like

It should only fire once per instance of i18next.

The api allows you to create and clone instances. I’m guessing it would be fired in those cases but I haven’t tested those yet as I can’t think of any use cases for cloning and creating an additional one. Probably used for weird setups.

Language changed is not fired a lot unless someone goes berserk with it.

I’ll be adding some logging and monitoring.

I understand that official documentation/support is coming soon…

What is the best way right now to work with external libraries on the front-end.

For example I’m integrating https://developers.cloudflare.com/stream/viewing-videos/using-the-player-api and right now I’m doing it like this:

Want to pause the video on button press:

  1. Add library & iframe to page:
  src="https://iframe.videodelivery.net/$VIDEOID"
  style="border: none;"
  height="720"
  width="1280"
  allow="accelerometer; gyroscope; autoplay; encrypted-media; picture-in-picture;"
  allowfullscreen="true"
  id="stream-player"
></iframe>

<script src="https://embed.videodelivery.net/embed/sdk.latest.js"></script>
  1. Add function to the page:
function pauseVideo() {
    player.pause()
}
  1. Add button to page → Dynamic Events → On click → Flow → RunJS → pauseVideo()

This is the best way I can think of right now to ‘Wapplerfy’ it.
I want to use the powers of wappler as much as possible to keep it easy to manage and develop further. We also need to hook into the events of the player, so I’m foreseeing a bunch of javascript code to create functions which are then called using RunJS. That doesn’t seem ideal.

Suggestions?

@JonL :smiley:

You can run javascript functions directly on click, no need of flow.
Button > Static Events > Click > function name here. That’s what the static events are used for.

1 Like

Are you going to integrate this library with data coming from Wappler or other Wappler components? If not, Teo’s method is the way to go. If not, custom component.

@Teodor
Ah okay thanks that will make it a bit cleaner.

@JonL Hmm not sure if this answers your question:

  • I’ll pass data from this library to the db using wappler (for exmaple, “how much time is watched from the video”
  • I want to manipulate a button based on this data: in my “pauseVideo” example I want that button to change to a play icon and also have a button press make the video play.
    Just like your challenge example video Custom App Connect components (The button becomes a ‘leave challenge’ and triggers a library event (The toast)

I think the conclusion is that a custom component would be the way to go @JonL? If yes: should I just follow patrick’s example here Custom App Connect components or do you have a better example I can build off? (Sorry this thread is huge, I’ll need a few hours to filter and see what the best way is)

@karh sorry for the late reply. Holidays got in the middle and I didn’t come to this thread again until myself had some questions about extending components.

I would go with a custom component, but it can get tricky as the documentation on them is scattered. I also reverse engineer App Connect a lot to fill some gaps. But still I run into roadblocks from time to time.

If it’s not a priority in your project I would postpone it until the team releases the feature.

@george @patrick I was playing with extending the components but it doesn’t work as I would expect. Extending a component doesn’t seem to expose the extended component methods(no inheritance).

Based on this feature request I tried to learn extending by implementing a custom component.

<script>
        dmx.Component("jonl-browser", {
        extends: "browser",
        methods: {
         reload: function () {
            location.reload();
         }
        }
    })
</script>

I would have expected that having the dmx-browser js file loaded on page I could trigger the alert() from the dmx-jonl-browser component but it seems I can’t.
So there is no actual inheritance, right? Then my doubt is what does the extends attribute work for?

If there is no inheritance, wouldn’t it make sense to refactor the code into JS classes from ES2015 in order to give us the possibility to extend and inherit?

Hope your holidays were good :smiley: Thanks! I’m going to try wait with the custom component for better support

1 Like

@patrick I think you are the one now holding the castle. What’s your take on extending core components and inheritance as per my previous post?