Asynchronous Bootstrap Popover Content

Asynchronously retrieve data with which to populate a Bootstrap popover.

by Lars Holm Jensen

HTML

<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.1/js/bootstrap.min.js"></script>
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css">
<script src="//cdnjs.cloudflare.com/ajax/libs/bluebird/1.2.2/bluebird.js"></script>
<button id="show-menu" class="btn btn-success" type="button" data-toggle="popover"><span class="glyphicon glyphicon-plus"></span> Create</button>
<div id="messages"></div>

<script type="text/template" id="data">
    <ul class="list-unstyled">
        <li class="create"><button id="foo" class="btn btn-link">Foo</button></li>
        <li class="create"><button id="bar" class="btn btn-link">Bar</button></li>
        <li class="create"><button id="baz" class="btn btn-link">Baz</button></li>
    </ul>
</script>

CSS

/* needed to position the spinner correctly */
.loading {
    position: relative;
}

/* credit: http://stephanwagner.me/only-css-loading-spinner */
@keyframes spinner {
    to { transform: rotate(360deg); }
}
@-webkit-keyframes spinner {
    to { -webkit-transform: rotate(360deg); }
}
.spinner {
    min-width: 24px;
    min-height: 24px;
}
.spinner:before {
    content:'Loading…';
    position: absolute;
    top: 50%;
    left: 50%;
    width: 16px;
    height: 16px;
    margin-top: -10px;
    margin-left: -10px;
}
.spinner:not(:required):before {
    content: '';
    border-radius: 50%;
    border-top: 2px solid #03ade0;
    border-right: 2px solid transparent;
    animation: spinner .6s linear infinite;
    -webkit-animation: spinner .6s linear infinite;
}

JavaScript

// simulate a long-running task
function getDataAsync() {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            resolve($('#data').html());
        }, 3000);
    });
}

// register popover on button
$('#show-menu').popover({
    container: 'body',
    content: function () {
        // credit: http://stackoverflow.com/a/14560039
        // create a placeholder id
        var tmpId = 'tmp-id-' + $.now();
        // initiate data retrieval and attach a success handler
        getDataAsync().then(function (data) {
            $('#' + tmpId).removeClass('loading spinner').html(data);
        });
        // generate temporary content for the placeholder to show the user while we wait
        return $('<div>').attr('id', tmpId).addClass('loading spinner');
    },
    html: true,
    placement: 'bottom',
    title: 'Create a thing',
    trigger: 'hover'
});

// register click handler for dynamically-generated DOM elements
$(document).on('click', '.create', function (e) {
    // manually hide the popover since we aren't interacting with it directly
    $('#show-menu').popover('hide')
    // do something creative with the data
    $('#messages').append('Created a ' + $(e.target).attr('id') + ' and released it into the wild!<br/>');
});