Building a WeChat Media Resource Selector Component with KnockoutJS

Implementation Examples

Below are practical examlpes demonstrating how to integrate the component into your templates:

<!-- Basic usage for media ID selection -->
<script id="mediaIdTemplate" type="text/html">
    <media-choice-button params="bindingValue: media_id"></media-choice-button>
</script>

<!-- Advanced usage: News article selection with locked type -->
<script id="articleTemplate" type="text/html">
    <media-choice-button params="bindingValue: media_id, defaultType: 5, isSelectionDisabled: true"></media-choice-button>
</script>

Configuration Parameters

  • bindingValue: An observable holding the WeChat media identifier.
  • defaultType: Specifies the initial media category. Accepted values are: 0 (Image), 1 (Audio), 2 (Video), 5 (News Article).
  • isSelectionDisabled: A boolean flag to prevent users from changing the media type dropdown.

Component Source Code

The following implementation defines the ViewModel and registers the custom component with KnockoutJS:

(function() {
    'use strict';

    // ViewModel factory function
    var createComponentInstance = function (params, componentInfo) {
        var vm = this;

        // Observable properties for UI binding
        vm.displayTitle = ko.observable('Select Material');
        vm.previewUrl = ko.observable('/Content/placeholders/default-thumbnail.png');
        vm.selectedMediaId = ko.observable('');

        // Configuration state
        vm.currentMediaType = ko.observable('1');
        vm.lockMediaType = ko.observable(false);

        // Reset display when media type changes
        vm.currentMediaType.subscribe(function (newType) {
            console.log('Media type switched to:', newType);
            vm.displayTitle('Select Material');
            vm.previewUrl('/Content/placeholders/default-thumbnail.png');
            vm.selectedMediaId('');
        });

        // Open the resource picker modal
        vm.openResourcePicker = function () {
            var mediaType = vm.currentMediaType();
            var pickerUrl = '';

            var urlMappings = {
                '5': '/Site_News?resourceType=5&lightLayout=1', // News
                '0': '/Site_Resources?resourceType=0&lightLayout=1', // Image
                '1': '/Site_Resources?resourceType=1&lightLayout=1', // Audio
                '2': '/Site_Resources?resourceType=2&lightLayout=1'  // Video
            };

            pickerUrl = urlMappings[mediaType] || '';
            if (pickerUrl) {
                mwc.window.show('Choose Material', pickerUrl);
            }
        };

        // Initialize from passed parameters
        if (params && typeof params.bindingValue() === 'string') {
            // Sync external value with internal state
            params.bindingValue.subscribe(function (newValue) {
                vm.selectedMediaId(newValue);
            });

            // Load existing media data if available
            if (params.bindingValue().length > 0) {
                vm.selectedMediaId(params.bindingValue());

                mwc.restApi.get({
                    url: '/Site_Resources/GetJsonDataByMediaId/' + vm.selectedMediaId(),
                    isBlockUI: true,
                    blockUI: componentInfo.element,
                    success: function (response) {
                        var resourceData = response.FileBase;
                        if (!resourceData) return;

                        // Update type first to avoid subscription clearing data
                        vm.currentMediaType(response.ResourceType);
                        vm.displayTitle(resourceData.Name);
                        vm.previewUrl(resourceData.FrontCoverImageUrl || resourceData.Url || resourceData.SiteUrl);
                        vm.selectedMediaId(resourceData.MediaId);
                    }
                });
            }

            // Apply configuration overrides
            if (params.defaultType !== undefined) {
                vm.currentMediaType(String(params.defaultType));
            }
            if (params.isSelectionDisabled !== undefined) {
                vm.lockMediaType(params.isSelectionDisabled);
            }
        }

        // Global callback for resource selection
        window.handleResourceSelection = function (resource) {
            params.bindingValue(resource.MediaId);
            vm.selectedMediaId(resource.MediaId);
            vm.displayTitle(resource.Name);
            vm.previewUrl(resource.FrontCoverImageUrl || resource.Url || resource.SiteUrl);
        };
    };

    // Component registration
    ko.components.register('media-choice-button', {
        viewModel: {
            createViewModel: function (params, componentInfo) {
                return new createComponentInstance(params, componentInfo);
            }
        },
        template:
            '<div class="form-group">' +
                '<label class="col-sm-2 control-label">Type:</label>' +
                '<div class="col-sm-10">' +
                    '<select class="form-control" data-bind="value: currentMediaType, disable: lockMediaType">' +
                        '<option value="0">Image</option>' +
                        '<option value="1">Audio</option>' +
                        '<option value="2">Video</option>' +
                        '<option value="5">News Article</option>' +
                    '</select>' +
                '</div>' +
            '</div>' +
            '<div class="form-group">' +
                '<div class="col-sm-12">' +
                    '<div class="panel panel-default" data-bind="click: openResourcePicker">' +
                        '<div class="panel-body">' +
                            '<!-- Image preview -->' +
                            '<p data-bind="if: currentMediaType() === \'0\'">' +
                                '<img style="width:150px;height:100px" data-bind="attr: {src: previewUrl}" />' +
                            '</p>' +
                            '<!-- Audio player -->' +
                            '<p data-bind="if: currentMediaType() === \'1\' && selectedMediaId() !== \'\'">' +
                                '<audio style="width:250px;height:100px" controls data-bind="attr: {src: previewUrl}"></audio>' +
                            '</p>' +
                            '<!-- Video player -->' +
                            '<p data-bind="if: currentMediaType() === \'2\' && selectedMediaId() !== \'\'">' +
                                '<video style="height:100px;width:250px" controls data-bind="attr: {src: previewUrl}"></video>' +
                            '</p>' +
                            '<!-- News article thumbnail -->' +
                            '<p data-bind="if: currentMediaType() === \'5\'">' +
                                '<img style="width:150px;height:100px" data-bind="attr: {src: previewUrl}" />' +
                            '</p>' +
                            '<div>' +
                                '<p data-bind="text: displayTitle"></p>' +
                            '</div>' +
                            '<!-- Placeholder button for empty audio/video -->' +
                            '<!-- ko if: selectedMediaId() === \'\' && ["1", "2"].includes(currentMediaType()) -->' +
                            '<p>' +
                                '<a class="btn btn-primary btn-block m-t" data-bind="click: $root.openResourcePicker">' +
                                    '<i class="fa fa-plus"></i> Select Material' +
                                '</a>' +
                            '</p>' +
                            '<!-- /ko -->' +
                        '</div>' +
                    '</div>' +
                '</div>' +
            '</div>'
    });
})();

Component Output

The rendered component provides a dropdown selector for media types and a clickable panel area that displays the selected resource preview with appropriate rendering based on the content type.

Tags: KnockoutJS WeChat API Custom Components javascript MVVM Pattern

Posted on Thu, 18 Jun 2026 17:04:19 +0000 by shmony