Confirm Button

logo

I’m a doofus. I’ve long since stopped making excuses and just accpeted the fact that if a delicate item can be dropped, or an unintended system can be trigged, or a wrong button can pushed, I’m very likely going to drop, trigger, or push it. Case in point: I have a few buttons in my OH UI that are extremely useful, but only under very certain circumstances (for example, a button that turns off all the lights in the house). Naturally, I have the tendancy to occasionally press these buttons during other circumstances. My son is tired of being plunged into darkness while he’s doing homework, and my wife has a similar objection when she’s in the kitchen baking and everything goes dark just because I pushed the wrong button (again). So, I have to make sure that my “smart house” is smarter than me and fool-proof (me-proof) various parts of my OH UI.

For the particular problem of the important button that should not be pressed accidentally, I have created this widget: a button that requires a second confirmation push within a certain timeframe before the associated action is triggered.

Usage

This widget should be pretty close to a drop-in replacement for any basic oh-button (except for the action definition, see Actions below). It takes some of the same relevant parameters, and takes it’s default style from the oh button classes.

When the user clicks on the button no action is taken, but the user is asked to confirm the activation of the button and a countdown starts (indicated by the decreasing progress bar on the bottom of the button). A second click during that countdown is required to confirm that the action should be executed. If no second click is made during the countdown then the button reverts back to its original condition.

confirm

Configuration

Parameter Description Default
barColor Color of the displayed countdown bar OH theme color
confirmText Text to display when confirmation is required Confirm
countdown Countdown time (in seconds) 10
raised Same as oh-button raised property false
style Same as oh-button style property* none
text Text to display on the basic button none

Actions

All the standard action configurations are accessible but must have the prefix confirmed_ added to the them. For example, to configure the button to send a command to an item you would use confirmed_acton, confirmed_actionCommand, and confirmed_actionItem.

* - The style property can only be set when configuring the button in a code editor, it cannot be properly set using the Props dialog. This is because it must be an object which is easily defined using yaml but cannot currently be expressed via the dialog.

Changelog

Version 0.1

  • initial release

Resources

https://raw.githubusercontent.com/JustinGeorgi/oh-public/main/widgets/confirm_button.yaml

22 Likes

Hi Justin,

many thanks for this UI Widget . I also have some extreme useful but dangerous buttons on my pages and this widget will help.

I do have a question on the style. I would like to have the button the same size look and feel as my other label cells and the text appearing on the horizontal and vertical cantered. I have started to change your yaml code and had to use a absolute height as otherwise the button size did not change. The rounded corners I found some tips to use the border-radius. But all my attempts to get the initial text and the confirm text vertically aligned failed :frowning:

slots:
  default:
    - component: div
      config:
        class:
          - cd-button
        style:
          --countdown-background: =props.style && props.style.background
          --countdown-bar-color: =props.barColor
          --countdown-duration: =`${(props.countdown || '10')}s`
          height: 120px
          border-radius: 20px
          position: relative
          text-align: center
          vertical-align: middle

Also tried to edit not the default slot but the two oh-button yaml code directly as they seem to “overlap” each other. From the way I have written this you can probably guess that I am not a CSS expert…

Any quick idea or suggestion you have in mind to achieve this. For better illustration below a picture. Ideal case I also ike to have a icon displayed but this is not a must have

Thanks
bkumio

Vertical alignment with css is sometimes tricky, IMO. The best advice is always to let the css do as much of it as possible and not try to force it. What I mean in this case, is don’t set any of the child element heights to an absolute value. Let them all just be the same height as the root component with height: 100% (add that to the style of the cd-button <div> and the two oh-buttons). Then all you need to do is set the height of whole widget using the style parameter when you add it to your page.

Now you can easily control the height of the whole widget, but that doesn’t fix the alignment issue. I think here, I would just rely on flexbox for the alignment. That will require, I believe, the fewest adjustment. Those adjustments (which should be applied to both buttons) are:

  1. Move the text from the button definition into a Label in the buttons default slot (this will render the text in a <div> element instead of the default <span>).
  2. Set the button to use flex display.
  3. Set the flex pattern to use the column (up-down direction) and then to center the child elements

For example, for the confirm button, that would look like this:

          - component: oh-button
            config:
              actionPropsParameterGroup: confirmed
              class: =(vars.countdown && 'cd-fade')
              clearVariable:
                - countdown
              style:
                visibility: hidden
                height: 100%                  <-- Make sure button is always full height of the root element
                display: flex                 <-- Set to use flexbox for displaying child elements
                flex-direction: column        <-- Align the child elements in a column
                justify-content: center       <-- Start that alignment in the center of the button
            slots:
              default:
                - component: Label            <-- Make the button text a `<div>` child element
                  config:
                    text: =props.confirmText || 'Confirm'

Edit: If you stretch the height of the widget, the countdown bar at the bottom will also increase in size. If you want it to have an absolute size, for example, 4px then you have to change the background-size definition in the cd-fade style declaration in the cd-button component:

background-size: 100% calc(100% - 4px), 200% 4px;

Just want to express my appreciation for this widget which is kind of a masterclass in authoring them, because it showcases techniques that are rather obscure or not documented well enough, like:

  parameterGroups:
    ...
    - name: confirmed
      context: action
      label: Action with confirmation
...
          - component: oh-button
            config:
              actionPropsParameterGroup: confirmed
        style:
          --countdown-duration: =`${(props.countdown || '10')}s`
...
(in raw CSS but could also be in a "style" property):

            animation-duration: var(--countdown-duration);
  • Losing the spaces in ternary expressions so you don’t have to enclose them in quotes in your YAML:
   - =(props.raised)?'button-raised':''
1 Like

This worked, many thanks

I did not had to use your #1 suggestion to move the button definition into a label default slot. The change #2 and #3 in combination with the widget style parameter and and the usage of height: 100% did already rendered both texts in the middle the way I wanted. Don’t know if this is by accident on on intention.
This CSS stuff is not intuitive/easy to read if you are not a developer but I am getting there slowly.

As well the hint for the bacground-size was useful to experiment with. Didn’t needed at the end as it looked good for my box height of approx. 120px

I do have one final question. The standard labels do have a sort of semitransparent blurred box-shadow style. Any idea how to achieve this too? On w3schools.com I do find some examples on this like : box-shadow: 5px 10px 8px #888888 . But I don’t know how to translate this into this Yaml code at the right line(s)

Edit: reading Yannick´s reply its no wonder I struggled with adapting a masterpiece type of example :slight_smile: . Thanks for all your support and patience guys.

You are correct, box-shadow is the css property you are looking for. Even better you don’t have to worry about figuring out what the settings are because the all the oh cards just use the built-in f7 variable for shadow f7-card-expandable-box-shadow.

In this case, I would advice against trying to make that part of the widget code and just add it to the style wherever you are using the widget itself.

- component: widget:mod_timed_button
  config:
    confirmed_action: toggle
    confirmed_actionCommand: ON
    confirmed_actionCommandAlt: OFF
    confirmed_actionItem: SomeRandomSwitch
    countdown: 3
    raised: true
    text: Some Confirmation Button
    style:
      box-shadow: var(--f7-card-expandable-box-shadow)   

The reason for this recommendation is two-fold: 1) this way you can use the widget in other situations where you might not want the shadow, 2) the way the widget allows for setting the style is by passing the style configuration object of the overall widget directly to the root component with this line:

style: =props.style

Unfortunately, the widget expressions don’t support combining two objects, so it is not trivial to use that externally defined style object at the same time that you define some internal styles.

Thanks Justin, this worked.

Although I had to experiment with a couple parameters to make it look like the normal label buttons. Probably not the best parameter set but at least it looks like the normal other label buttons in the column/row setup on the page. But thanks again this widget is exactly what I was looking for.

For completeness I have attached my current Cell YAML Config and the Widget code. From my end, on what I wanted to achieve, it looks good.

The behaviour was somewhat strange for example the height:100% in the YAML code did not worked I had explicitly use 120px to have the height I wanted. For the with I had to use 93% as otherwise the spacing between the label would be too small/non existent. In the widget code I used position: absolut as otherwise the individual buttons are not appearing over each other (don’t know how to describe this better) …

Perhaps with your experience you find something a way that the same result can be done better? Still learning on this CSS F7 framework stuff

YAML WidgetConfig:

component: widget:jag_confim_button
config:
  barColor: GREEN
  confirmText: Bestätigen
  confirmed_action: command
  confirmed_actionCommand: ON
  countdown: 5
  raised: true
  style:
    border-radius: var(--f7-card-expandable-border-radius)
    box-shadow: var(--f7-card-expandable-box-shadow)
    confirmed_actionItem: MyPersonalActionItem
    height: 120px
    margin: 10px
    width: 93%
  text: Kommen

WidgetCode itself

uid: jag_confim_button
tags:
  - marketplace:146490
props:
  parameters:
    - description: Button text
      label: Text
      name: text
      required: false
      type: TEXT
      groupName: buttonConf
    - description: Button is raised
      label: Raised
      name: raised
      required: false
      type: BOOLEAN
      groupName: buttonConf
    - description: Confirmation text
      label: Confirm Text
      name: confirmText
      required: false
      type: TEXT
      groupName: buttonConf
      advanced: true
    - description: Confirmation countdown duration
      label: Duration (seconds)
      name: countdown
      required: false
      type: INTEGER
      groupName: otherConf
    - description: Countdown progress bar color
      label: Bar color
      name: barColor
      required: false
      type: TEXT
      groupName: otherConf
      advanced: true
    - description: Only works in text configuration
      label: Style
      name: style
      required: false
      type: TEXT
      groupName: otherConf
      advanced: true
  parameterGroups:
    - name: buttonConf
      label: Button configuration options
    - name: confirmed
      context: action
      label: Action with confirmation
    - name: otherConf
      label: Other Configurations
timestamp: Jun 13, 2023, 8:34:36 PM
component: div
config:
  class:
    - button
    - =(props.raised)?'button-raised':''
  key: =Math.random() + ((vars.countdown && vars.countdown.toString()) || '0')
  style: =props.style
slots:
  default:
    - component: div
      config:
        class:
          - cd-button
        style:
          --countdown-background: =props.style && props.style.background
          --countdown-bar-color: =props.barColor
          --countdown-duration: =`${(props.countdown || '10')}s`
          background: white
          border-radius: 15px
          display: flex
          flex-direction: column
          font-size: 24px
          height: 100%
          justify-content: center
          left: 0
          position: absolute
          top: 0
          width: 100%
        stylesheet: >
          .cd-fade {
            animation-name: count-down-fade;
            animation-duration: var(--countdown-duration);
            animation-timing-function: linear;
            background: none, linear-gradient(90deg, var(--countdown-bar-color, var(--f7-theme-color)) 50%, var(--countdown-background, var(--f7-button-bg-color)) 50%);
            background-size: 100% 90%, 200% 10%;
            background-position-y: 0%, 100%;
            background-color: var(--countdown-background, var(--f7-button-bg-color)) !important;
            background-repeat: no-repeat;
          }

          @keyframes count-down-fade {
            from {
              background-position-x: 0%, 0%;
              visibility: visible;
            }
            to {
              background-position-x: 0%, 100%;
              visibility: visible;
            }
          }

          .cd-hide {
            animation-name: count-down-hide;
            animation-duration: var(--countdown-duration);
            animation-timing-function: linear;
          }

          @keyframes count-down-hide {
            from {
              visibility: hidden;
            }
            to {
              visibility: hidden;
            }
          }
      slots:
        default:
          - component: oh-button
            config:
              actionPropsParameterGroup: confirmed
              class: =(vars.countdown && 'cd-fade')
              clearVariable:
                - countdown
              style:
                border-radius: 15px
                color: black
                display: flex
                flex-direction: column
                font-size: 24px
                height: 100%
                justify-content: center
                left: 0
                position: absolute
                top: 0
                visibility: hidden
                width: 100%
              text: =props.confirmText || 'Confirm'
          - component: oh-button
            config:
              action: variable
              actionVariable: countdown
              actionVariableValue: =(vars.countdown || 0) + 1
              class: =(vars.countdown && 'cd-hide')
              style:
                background: white
                border-radius: 15px
                color: black
                display: flex
                flex-direction: column
                font-size: 24px
                height: 100%
                justify-content: center
                left: 0
                position: absolute
                top: 0
                width: 100%
              text: =(props.text || '')

1 Like

Hi Justin,

I’ve added a widget to the marketplace which has several oh-knob-cell buttons with code similar to this:

    - component: f7-row
      config:
        class:
          - margin
      slots:
        default:
          - component: f7-col
            config:
              width: "100"
            slots:
              default:
                - component: oh-knob-cell
                  config:
                    action: toggle
                    actionCommand: true
                    actionCommandAlt: false
                    actionItem: =(props.mainswitch)
                    actionOptions: ON,OFF
                    color: "=(items[props.mainswitch].state === 'ON') ? 'green' : 'red'"
                    commandInterval: 500
                    expandable: false
                    footer: "=(items[props.mainswitch].state === 'ON') ? 'ACTIVE' : 'DISABLED'"
                    icon: "=(items[props.mainswitch].state === 'ON') ? 'iconify:mingcute:power-fill' : 'iconify:mingcute:power-line'"
                    iconColor: "=(items[props.mainswitch].state === 'ON') ? 'green' : 'red'"
                    releaseOnly: true
                    responsive: true
                    style:
                      text-shadow: 1px 1px black
                    title: Main Switch

I read this thread and your yaml code over and over and have some questions…

Using advanced css is a barely unknown territory for me!

My goal is to add the functionality to every oh-button-cell in my code but I’m struggling how to implement this. My idea was to override the footer-text with the confirmation-text when the button is pressed until confirm or timeout. The props for button-type, button-raised and button-name are not necessary for my use case.

Do you think it is possible at all to get this working like expected or with slight adjustments? And if the answer would be yes:

  • Is it necessary to use the two divs with hidden function or can I implement this styles directly to the cells?
  • where would the stylesheet part go, do I have to implement this to every cell or can I use the stylesheet in several cells simultaniously?
  • Where would the style and classes and key go?
  • where would the div classes and styles go?

Sorry, but I’d really like to understand what’s going on ‘under the hood’ and use this awesome idea for my widget… Maybe you can take a little time to answer my questions - I would be very grateful for some help on this

I tried to learn something by myself and exchanged the divs with oh-button-cells and mixed some config from the div and my cell config… it shows something but I always get the confirmed popup text which I added as Action feedback without any countdown…

this is my approach in a test widget so far

uid: TestButton with Confirmation
tags: []
props:
  parameters:
    - description: Confirmation text
      label: Confirm Text
      name: confirmText
      required: false
      type: TEXT
      groupName: buttonConf
      advanced: true
    - description: Confirmation countdown duration
      label: Duration (seconds)
      name: countdown
      required: false
      type: INTEGER
      groupName: otherConf
    - description: Countdown progress bar color
      label: Bar color
      name: barColor
      required: false
      type: TEXT
      groupName: otherConf
      advanced: true
    - description: Only works in text configuration
      label: Style
      name: style
      required: false
      type: TEXT
      groupName: otherConf
      advanced: true
  parameterGroups:
    - name: buttonConf
      label: Button configuration options
    - name: confirmed
      context: action
      label: Action with confirmation
    - name: otherConf
      label: Other Configurations
timestamp: Sep 7, 2023, 8:41:36 PM
component: f7-card
slots:
  default:
    - component: oh-knob-cell
      config:
        actionPropsParameterGroup: confirmed
        clearVariable:
          - countdown
        color: "=(items[props.mainswitch].state === 'ON') ? 'green' : 'red'"
        footer: =props.confirmText || 'Confirm'=props.confirmText || 'Confirm'
        icon: "=(items[props.mainswitch].state === 'ON') ? 'iconify:mingcute:power-fill' : 'iconify:mingcute:power-line'"
        iconColor: "=(items[props.mainswitch].state === 'ON') ? 'green' : 'red'"
        releaseOnly: true
        responsive: true
        title: Main Switch
        expandable: false
        class:
          - button
          - =(props.raised)?'button-raised':''
          - =(vars.countdown && 'cd-fade')
        key: =Math.random() + ((vars.countdown && vars.countdown.toString()) || '0')
        style: =props.style
      slots:
        default:
          - component: oh-knob-cell
            config:
              actionVariable: countdown
              actionVariableValue: =(vars.countdown || 0) + 1
              action: variable
              color: "=(items[props.mainswitch].state === 'ON') ? 'green' : 'red'"
              commandInterval: 500
              footer: =(props.text || '')
              icon: "=(items[props.mainswitch].state === 'ON') ? 'iconify:mingcute:power-fill' : 'iconify:mingcute:power-line'"
              iconColor: "=(items[props.mainswitch].state === 'ON') ? 'green' : 'red'"
              releaseOnly: true
              responsive: true
              expandable: false
              class:
                - =(vars.countdown && 'cd-hide')
                - cd-button
              style:
                --countdown-background: =props.style && props.style.background
                --countdown-bar-color: =props.barColor
                --countdown-duration: =`${(props.countdown || '10')}s`
                display: flex
                flex-direction: column
                height: 100%
                justify-content: center
                left: 0
                position: absolute
                top: 0
                width: 100%
              stylesheet: >
                .cd-fade {
                  animation-name: count-down-fade;
                  animation-duration: var(--countdown-duration);
                  animation-timing-function: linear;
                  background: none, linear-gradient(90deg, var(--countdown-bar-color, var(--f7-theme-color)) 50%, var(--countdown-background, var(--f7-button-bg-color)) 50%);
                  background-size: 100% 90%, 200% 10%;
                  background-position-y: 0%, 100%;
                  background-color: var(--countdown-background, var(--f7-button-bg-color)) !important;
                  background-repeat: no-repeat;
                }

                @keyframes count-down-fade {
                  from {
                    background-position-x: 0%, 0%;
                    visibility: visible;
                  }
                  to {
                    background-position-x: 0%, 100%;
                    visibility: visible;
                  }
                }

                .cd-hide {
                  animation-name: count-down-hide;
                  animation-duration: var(--countdown-duration);
                  animation-timing-function: linear;
                }

                @keyframes count-down-hide {
                  from {
                    visibility: hidden;
                  }
                  to {
                    visibility: hidden;
                  }
                }
            slots: []

The basic idea is this: The action of the primary button just increments a variable (if the variable doesn’t exist then it substitutes 0 and increments to 1). When that variable is a non-false value then the two different buttons get new css classes. Those new css classes trigger a css animation that runs for the configured amount of time.

The most important part here is that in both animations the css property animation-fill-mode is not set so it retains its default value of none. This property determines how an animation affects an element before and after the animation runs. A value of none means that the animation is not applied at all except when it is running. So, as soon as the animation is over, the two button return to whatever css properties they had before the animation was triggered. The animations then switch the visibility of the two buttons: with no animation the primary button is visible and the confirm button is not, when the animations are running the the confirm button is visible and the primary button is not.

If the confirm button is pressed while it is visible, then it does two things: it runs whatever the actual configured action is and it clears the variable set by the primary button (thus removing the css classes and consequently ending the css animation).

The ‘key’ property is required on one of the parent elements to handle the case where the confirm button is not pushed while it is visible. In that case, the animation has run its course and ended so that the primary button is visible once again. But the variable is still non-zero so both buttons still have their css classes applied. Incrementing the variable again won’t change that and so wouldn’t cause the animations to restart. But, changing the variable will change the value of the key property and when that value changes that element and all of its child elements will be redrawn, this does cause the animation to run again, because it’s as if it has never run in the first place.

The countdown bar is just a feature of the css animation on the confirm button itself. The button just has a background that is twice as wide as the button and where the bottom is one half transparent and one half colored. During the course of the animation, that background slides across the button so you see the boundary between transparent and colored background sections as the leading edge of the “countdown bar”.

Partially. Some aspects of this are generic enough that you could adapt them fairly easily to an oh-cell. Others unfortunately might be trickier.

You have to have the two different buttons (or in your case cells). There is just no way for the oh widget components to know when a css animation is running or not so there’s no way to make a single component aware of whether it should be in the base state or the confirm state. And, because you need the two different components, you need to have at least on parent element to contain them and hold the css directives that apply to both of them.

You don’t necessarily need the two nested div element as parents, that’s an artifact of needing ot be able to pass the style object from the config to the widget while also being able to set some style parameters directly. If you’re not try to set this up with a configurable style then you can get away with only one parent element.

The C in css stands for ‘cascading’ which refers to the idea that a css setting in one element will always apply to all of this child elements unless it is specifically overridden by one of teh child element’s own settings. So, because you want the css of the classes and animations to apply to both of the child cells you would need to have that stylesheet in a parent of those two cell. If you are putting many of these on a single page, then instead of having the css directives in the stylesheet of each parent element, you can define a single stylesheet for the page that sets of the classes and animations. That would have to be true on every page you put one of these on however.

You might be able to get away with defining the key on each of the two cells separately. But there’s no reason to since you still must have the parent element. It’s best to just leave it there.

This is easy if you are using the two different cells. The “confim” cell just has whatever the override text is in the footer. And then, if you don’t want the countdown bar, you don’t need any of the background parameters in the classes or animation keys. But again, because you cannot integrate the css animation status into the widget expressions there would be no way to do this just with an expression for the cell footer on one cell.

Right now your widget structure is:

f7-card
  oh-knob-cell
    oh-knob-cell <-- css styles applied here
  1. you do not want one cell as a child of the other
  2. if your css settings are on the lowest child element then they cannot apply to the elements above
1 Like

Thanks a ton! I think You covered mostly everything in your explanation… I now understand more how it’s supposed to work, and will give it another try tomorrow and see how far it’ll take me without further help :wink:

Thank you so much for everything!

What type of element would you choose for this - am I able to use the styles in a f7-card?

I chose a div element because that will have no additional styling applied to it. It is there to hold the styles that should apply to the button, and it shouldn’t do anything else. Any of the main f7- components (card, block, row, etc.) are just rendered as div elements but with lots of additional f7 styling already applied to them which you don’t need. So, just cut out the middle-man and use the div.

1 Like

Whoo, I think I give up on this… I tried to use the browser to debug the css but there are so many classes and styles applied to an oh-knob-cell that it’s nearly impossible for me to work around these.

Maybe I am able to style divs to look like knob-cells and apply your functionality to it, but as you mentioned before there is way too much css in f7 or oh stuff… when I find a way to get this going I will post an update.