2

I'm creating a custom Merge Check for Bitbucket. I started by following this tutorial: https://developer.atlassian.com/server/bitbucket/how-tos/hooks-merge-checks-guide/

I want the view to be dynamic, e.g. have button that creates multiple similar input fields (of specified IDs), which eventually get stored in the config.

First of all, I used soy for this - I created static template with call to one e.g. .textField. It worked okay, but I couldn't create new, similar fields on the fly (after pressing 'Add new' button).

So I use JavaScript to get data from soy's config. I rewrite the whole config to JS "map" and then render all the fields dynamically (by appending them to HTML code), filling them with values from configuration or creating new fields by pressing the button.

It works - I get all the data saved in config for keys like field_[id], e.g field_1, field_2 etc.

But there's a bug. When I press the "Save" button and view the pop-up for editing once again, I can see the JavaScript get executed twice: I get all my fields rendered two times - first time during first execution and second time during the second, appearing just a few seconds later. There's no such problem when I save the configuration, refresh the page and then view the pop-up once again.

Here's my merge check's configuration in atlassian-plugin.xml file:

<repository-merge-check key="isAdmin" class="com.bitbucket.plugin.MergeCheck" name="my check" configurable="true">
        <config-form name="Simple Hook Config" key="simpleHook-config">
            <view>hook.guide.example.hook.simple.myForm</view>
            <directory location="/static/"/>
        </config-form>
    </repository-merge-check>

And my simplified .soy template code:

{namespace hook.guide.example.hook.simple}

/**
 * @param config
 * @param? errors
 */
{template .myForm}
    <script type="text/javascript">
            var configuration = new Object();

            {foreach $key in keys($config)}
                configuration["{$key}"] = "{$config[$key]}";
            {/foreach}

            var keys = Object.keys(configuration);

            function addNewConfiguration() {lb}
                var index = keys.length;
                addNewItem(index);
                keys.push("field_" + index);
            {rb}


            function addNewItem(id) {lb}
                var html = `<label for="field_${lb}id{rb}">Field </label><input class="text" type="text" name="field_${lb}id{rb}" id="branch_${lb}id{rb}" value=${lb}configuration["field_" + id] || ""{rb}><br>`;
                document.getElementById('items').insertAdjacentHTML('beforeend', html);
            {rb}

            keys.forEach(function(key) {lb}
                var id = key.split("_")[1];
                addNewItem(id);
            {rb});

             var button = `<button style="margin:auto;display:block" id="add_new_configuration_button">Add new</button>`;
             document.getElementById('add_new').innerHTML = button;
             document.getElementById('add_new_configuration_button').addEventListener("click", addNewConfiguration);

    </script>

    <div id="items"></div>
    <div id="add_new"></div>

    <div class="error" style="color:#FF0000">
        {$errors ? $errors['errors'] : ''}
    </div>
{/template}

Why does JavaScript get executed twice in this case? Is there any other way of creating such dynamic views?

magnes
  • 139
  • 1
  • 2
  • 11

1 Answers1

2

The soy template will get loaded and executed again whenever you click to edit the configuration. Therefore the javascript will also get executed again. To prevent this you can create a javascript file and put it next to your simpleHook-config.soy template file with the same filename, so simpleHook-config.js. The javascript file will be loaded automatically with your soy template, but once. Therefore you can hold a global initialisation state within a new introduced js object.

Furthermore, even though you are adding fields dynamically, you can still and should build the saved configuration inside the soy template already and not building it with javascript.

For me this approach works quite good (I wrote the code more or less blindly, so maybe you need to adjust it a bit):

In .soy file:

{namespace hook.guide.example.hook.simple}

/**
 * @param config
 * @param? errors
 */
{template .myForm}
<div id="merge-check-config">
    <div id="items">
        {foreach $key in keys($config)}
        {let $id: strSub($key, strIndexOf($key, "_") + 1) /}
        {call .field}
            {param id: $id /}
            {param value: $config[$key] /}
        {/foreach}
    </div>

    <div id="add_new">
        <button style="margin:auto; display:block" id="add_new_configuration_button">Add new</button>
    </div>

    <div class="error" style="color:#FF0000">
        {$errors ? $errors['errors'] : ''}
    </div>

    <script>
        myPluginMergeCheck.init();
    </script>
</div>
{/template}

/**
 * @param id
 * @param? value
 */
{template .field}
<div>
    <label for="field_${id}">Field</label>
    <input class="text" type="text" name="field_${id}" id="branch_${id}" value="${value}">
</div>
{/template}

In .js file:

myPluginMergeCheck = {
    initialized: false,
    init: function () {
        if (this.initialized) {
            return;
        }

        var me = this;
        AJS.$(document).on("click", "div#merge-check-config button#add_new_configuration_button"), function (event) {
            me.addNewItem();
        }
        this.initialized = true;
    },
    addNewItem: function() {
        var itemsDiv = AJS.$("div#merge-check-config div#items");
        var newId = itemsDiv.children("div").length;
        var html = hook.guide.example.hook.simple.field({id: newId});
        itemsDiv.append(html);
    }
};

magnes
  • 139
  • 1
  • 2
  • 11
TheFRedFox
  • 600
  • 3
  • 12
  • 1
    It still behaved the same for me. Without the `initialized` flag, the fields rendered twice. When using the flag, the pop-up initialized once, but was empty when opened again, because of setting the flag. What I did to fix it was adding a new event listener to the "Save/Enable" button, which sets the flag again to false. – magnes Jul 31 '19 at 08:18
  • Yet it still sounds like an extremely farfetched solution. Does anyone know the exact reason why the JS is executing twice during opening the pop-up once again after saving? – magnes Jul 31 '19 at 08:27
  • 1
    The reason why it is executed twice is, that the template is fetched and loaded again when opening the pop-up again. The javascript in the js file will, however, be loaded just once. Unfortunately, I still can't tell you why the fields are rendered twice. However, I try and come up with a better solution for your specific problem later this day. – TheFRedFox Jul 31 '19 at 13:35
  • 1
    A bit later, but just edit my answer now with hopefully a fully working version for you. – TheFRedFox Aug 01 '19 at 13:12
  • Interesting approach - very clear and readable :) Thank you! How about the atlassian-plugin.xml configuration? As we're using AJS here, it seems we need to configure some transformer, don't we? – magnes Aug 06 '19 at 14:30
  • 1
    Actually transformer is something I didn't touch yet, but if I get it correctly then: A transformer is there to batch web resource files, so is never needed mandatorily but can speed up things. There is also the `dependency` tag inside a `web-resource`, which is also not needed here, as the form will always be loaded when AJS is already loaded too. So, also for this it should not be needed. AJS can just be used directly here. – TheFRedFox Aug 06 '19 at 14:47
  • Thank you for your response. I asked about transformer, because when I don't use it, I get following error: "Uncaught TypeError: ((jQuery.event.special[handleObj.origType] || {}).handle || handleObj.handler).apply is not a function at HTMLDocument.dispatch (..." – magnes Aug 06 '19 at 15:00
  • 1
    Mhm, just looked in my plugin and I also am just using AJS straight ahead without any further configuration. Can you append to your question what you have now, the soy and js file and the atlassian-plugin.xml? – TheFRedFox Aug 06 '19 at 15:12
  • It actually turns out the situation is even weirder. The error above sometimes doesn't appear, leading to another one - "hook.guide.example.hook.simple.field is not a function". The problems appears for me even when using your exact .soy and .js file and configuration from the question. – magnes Aug 06 '19 at 15:25
  • The error appears when pressing the "Add new" button. – magnes Aug 06 '19 at 15:39
  • 1
    Ok, thats actually possible. I was writing this code blindly, but was thinking the call for the soy template would be correct like that. Can you open the Inspector of you browser while being on the config modal window and just type first "hook.guide.example.hook.simple.myForm" and then "hook.guide.example.hook.simple.field" in there and check if it returns something? – TheFRedFox Aug 06 '19 at 17:15
  • 1
    Already adjusted some code, as I was not calling the function correctly in js. Fixed that in my answer. It's in the js file. However, it should not be the cause of the error you were getting as it would have been producing another error. – TheFRedFox Aug 06 '19 at 17:29
  • Thank you for your help! :) After some time it turned out to be working without any changes... I'm not sure yet if it's deterministic, will be watching it carefully. For future use, do you recommend any documentation on Soy? I really miss e.g. list filtering here, I'm not sure if it's really impossible in soy or I'm just not aware of it. – magnes Aug 07 '19 at 10:02
  • 1
    Glad to hear, although I always find it fishy when it suddenly works. ^^ Unfortunately, I do not have THE recommendation. Although I am quite sure I saw some nice documentation for everything, I can not find it anymore and the official closure documentation redirects one for templates to the github repository: https://github.com/google/closure-templates There you can find some documentation, but it is not really well written IMO. There is also: https://developer.atlassian.com/server/confluence/writing-soy-templates-in-your-plugin/ which surely not covers everything. – TheFRedFox Aug 07 '19 at 13:45
  • 1
    A general approach: Template languages aren't there to manipulate data, so filtering might already be not possible. However, with an `if` inside a for loop or with a `continue` you still can somehow filter the data while traversing it. – TheFRedFox Aug 07 '19 at 13:47