Disabling a button with JQuery does not work at first time

Hi, i’m currently having a hard time with simply disabling buttons.
I have a template containing five buttons, and i want to disable some of them after they have been rendered.
This does not work when I do this initially, but if the user selects an item in a listview (which leads to disabling/enabling buttons), it works. But since the buttons are initially not correctly setup, they can be pressed, leading to unintended events.
I am using the latest Meteor release (1.7.0.5).
I cant stop thinking I am overlooking something obvious, since I spent hours trying to fix this.

defaultButtons.html

<template name="defaultButtons">
    <div class="bottom_fixed_buttons">
        <button id="default_button_add" class="default_button" disabled>Hinzufügen</button>
        <button id="default_button_duplicate" class="default_button" disabled>Duplizieren</button>
        <button id="default_button_delete" class="default_button" disabled>Löschen</button>
        <button id="default_button_edit" class="default_button" disabled>Bearbeiten</button>
        <button id="default_button_save" class="default_button" disabled>Speichern</button>
    </div>
</template>

defaultButtons.js

import { Template } from 'meteor/templating';

import './defaultButtons.html';
import './defaultButtons.less';

let isRendered = false;
let displayOptionStack = [];

//Values: hidden, disabled, enabled
//options: add, duplicate, delete, edit, save
export function setDisplayOption(option, value) {
    if (!isRendered) {
        displayOptionStack.push({opt:option,val:value});
        return;
    }
    
    switch(value) {
        case 'enabled':
            $('#default_button_' + option).css('display', 'initial');
            $('#default_button_' + option).prop('disabled', false);
            break;
        case 'disabled':
            $('#default_button_' + option).css('display', 'initial');
            $('#default_button_' + option).prop('disabled', true);
            break;
        default:
            $('#default_button_' + option).css('display', 'none');
    }
}

Template.defaultButtons.onRendered(function(){
    isRendered = true;
    for (let i=0; i<displayOptionStack.length; i++) {
        setDisplayOption(displayOptionStack[i].opt, displayOptionStack[i].val);
    }
    displayOptionStack = [];
});

Template.defaultButtons.onDestroyed(function(){
    isRendered = false;
    displayOptionStack = [];
});

listview.js (only the relating part)

import '/imports/ui/templates/defaultButtons/defaultButtons.js';
import DefaultButtons from '/imports/ui/templates/defaultButtons/defaultButtons.js';

Template.listview.onCreated(function () {
    console.log("[TEMPLATE]: listview created.");

    DefaultButtons.setDisplayOption("add", "enabled");
    DefaultButtons.setDisplayOption("duplicate", "disabled");
    DefaultButtons.setDisplayOption("delete", "disabled");
    DefaultButtons.setDisplayOption("edit", "hidden");
    DefaultButtons.setDisplayOption("save", "hidden");
// all are still enabled after these calls (but the hidden ones ar hidden)
}

you can try onRendered event.

The function calls which manipulate the DOM already take place in the onRendered function.
If they are called earlier, the call parametes are pushed on an array/stack and then called in onRendered.

The way to do this is to not use jQuery. That’s quite hard when you come to reactive, single page apps from a jQuery background - and I imagine we’ve all gone through that when we first started with Meteor.

The trick is to remember that Meteor is reactive, so it’s able to reactively change the DOM. In the case of enabling/disabling buttons in Blaze, the recommended solution is to use template helpers. So, for example, instead of:

<button id="default_button_add" class="default_button" disabled>Hinzufügen</button>

Use something like:

<button id="default_button_add" class="default_button" {{state}}>Hinzufügen</button>

and define a matching helper:

Template.defaultButtons.helpers({
  state() {
    // logic here to return 'hidden', 'enabled' or 'disabled' as appropriate.
  }

You can obviously do the same for css - although class is probably preferable.

You may be able to use Template#onRendered, as suggested by @minhna, but that doesn’t always work as you might expect.

Sometimes jQuery is a great solution, but it’s better to work with Meteor (or any reactive app), than against it.

Note that the “logic” needs to itself be reactive, so you will typically use reactive data sources (ReactiveVar, ReactiveDict, find etc.).

2 Likes

Thank you @robfallows for your detailed answer.
I tried your suggestion, but it completely prevents my listview from rendering, did I do something wrong?

defaultButtons.js

let displayOptions = new ReactiveVar({
    add: "hidden",
    duplicate: "hidden",
    delete: "hidden",
    edit: "hidden",
    save: "hidden"
});

//Values: hidden, disabled, enabled
//options: add, duplicate, delete, edit, save
export function setDisplayOption(option, value) {
    options = displayOptions.get();
    options[option] = value;
    displayOptions.set(options);
}

Template.defaultButtons.helpers({
    AddState(){if(displayOptions.get().add !== "enabled") return "disabled"},
    AddInvisible(){if (displayOptions.get().add === "hidden") return "invisible-button"},
//leaving out the other buttons since they have exactly the same logic

defaultButtons.html

<template name="defaultButtons">
    <div class="bottom_fixed_buttons">
        <button id="default_button_add" class="default_button {{AddInvisible}}" {{AddState}}>Hinzufügen</button>
<!-- other 4 buttons omitted -->
    </div>
</template>

The setup of the ReactiveVar should be done in the Template#onCreated:

Template.defaultButtons.onCreated(function() {
  this.displayOptions = new ReactiveVar({
    add: "hidden",
    duplicate: "hidden",
    delete: "hidden",
    edit: "hidden",
    save: "hidden"
  });
});

You would refer to that in your helper with Template.instance().displayOptions.

However, you should also write the buttons as Blaze components (templates). That will mean that each button has its own state. Instead of:

<template name="defaultButtons">
    <div class="bottom_fixed_buttons">
        <button id="default_button_add" class="default_button" disabled>Hinzufügen</button>
        <button id="default_button_duplicate" class="default_button" disabled>Duplizieren</button>
        <button id="default_button_delete" class="default_button" disabled>Löschen</button>
        <button id="default_button_edit" class="default_button" disabled>Bearbeiten</button>
        <button id="default_button_save" class="default_button" disabled>Speichern</button>
    </div>
</template>

Do something like:

<template name="defaultButtons">
  <div class="bottom_fixed_buttons">
    {{ >myButton id="default_button_add" name="Hinzufügen" }}
    {{ >myButton id="default_button_duplicate" name="Duplizieren" }}
    {{ >myButton id="default_button_delete" name="Löschen" }}
    {{ >myButton id="default_button_edit" name="Bearbeiten" }}
    {{ >myButton id="default_button_save" name="Speichern" }}
  </div>
</template>

<template name="myButton">
  <button id={{this.id}} class="default_button" {{state}}>{{this.name}}</button>
</template>
Template.myButton.onCreated(function() {
  this.status = new ReactiveVar('enabled');
});

Template.myButton.helpers({
  state() {
    return Template.instance().status.get();
  },
});

You may find a ReactiveDict is a cleaner approach that ReactiveVar when you’re tracking several variables.

This reference may help: http://blazejs.org/guide/reusable-components.html#Use-the-template-instance

1 Like

I tested your suggestion, it still wont disable the buttons. I did the following:

defaultButtons.js

Template.defaultButtons.onCreated(function(){
    console.log("[TEMPLATE] defaultButtons created.");
});

Template.defaultButtons.onRendered(function(){
    console.log("[TEMPLATE] defaultButtons rendered.");
});

Template.defaultButton.onCreated(function(){
    console.log("[TEMPLATE] defaultButton created.");
    this.state = new ReactiveVar("disabled");
    this.additionalClass = new ReactiveVar("");
});

Template.defaultButton.onRendered(function(){
    console.log("[TEMPLATE] defaultButton rendered.");
});

Template.defaultButton.helpers({
    State(){
        return Template.instance().state.get();
    },
    AdditionalClass(){
        return Template.instance().additionalClass.get();
    }
})

defaultButtons.html

<template name="defaultButtons">
    <div class="bottom_fixed_buttons">
        {{ >defaultButton id="default_button_add" name="Hinzufügen" }}
        {{ >defaultButton id="default_button_duplicate" name="Duplizieren" }}
        {{ >defaultButton id="default_button_delete" name="Löschen" }}
        {{ >defaultButton id="default_button_edit" name="Bearbeiten" }}
        {{ >defaultButton id="default_button_save" name="Speichern" }}
    </div>
</template>

<template name="defaultButton">
    <button id={{this.id}} class="default_button {{AdditionalClass}}" {{State}}>{{this.name}}</button>
</template>

This time I am pretty sure, I did nothing wrong, which is only more frustrating.
The buttons still are not disabled when rendered.
However, if I set additionalClass to invisible-button in onCreated, they are (correctly) invisible.
Could this in fact be a bug?

Hmm. I just copied and pasted your code and it works - the buttons are rendered, but disabled.

You are right, I’ve tested it in a new Project and it also works for me.
I guess I’m gonna have to find it myself since my project isn’t that small anymore.
But thanks for your help, this will probably help me in my future work!

1 Like