Support

Account

Home Forums General Issues Does ACF have a JS API?

Solving

Does ACF have a JS API?

  • I’d like to populate some fields programmatically. I know I could use PHP hooks, but for what I need, this is not a solution. I want to populate some custom fields based on images EXIF data – for example, populate the tags taxonomy with EXIF tags. Then I want a user to review the data before it will be saved to the database, so I have to do it on the front-end.

    Let’s say that I want to check taxonomy terms checkboxes programmatically.
    Is there any elegant way to do it (any undocumented JS API)? I know I could make some DOM manipulation with jQuery, but this is not a good and not a bulletproof solution for sure.

    // Edit.
    Eventually, If there’s no elegant way, I could update fields with PHP/AJAX and re-draw the field or even the whole fields group. Is there any way to force field/field-group re-render?

    I guess it is possible because this is exactly what happens when a field group is shown conditionally (for example depending on a page template).

  • Everything that ACF provides is pretty much documented here https://www.advancedcustomfields.com/resources/adding-custom-javascript-fields/ and there are no setting/getting functions that you can call from javascript. As it currently stands we are on our own for this, perhaps in the future there will be something added.

    Don’t know if this will interest you, but I have a few examples to dynamically updating fields here https://github.com/Hube2/acf-dynamic-ajax-select-example. I’m also in my spare time working on my own js extension for ACF that will do this type of thing, but it’s a slow process because I don’t really have a lot of spare time.

  • Thank you, John. You’ve confirmed my concerns – there’s no JS API.

    So I will probably update custom field value via AJAX, but I still need some way to re-render the field group with updated values – so basically I need to know how ACF handles field group rendering.

    I’m almost sure that it is possible (even if it’s not documented) because this is what happens if a field group is shown conditionally (for example if a field group is assigned to a specific page template and show/hides depending on a chosen page template).

    TL;DR
    I’d like to know how to call a field group re-render (or even better – a single field re-render) with JS.

    Thanks.

  • You’d have to look in the ACF js files, and I have looked. I could not find any way, for example to re-render an image field. If you look specifically at this file https://github.com/Hube2/acf-dynamic-ajax-select-example/blob/master/dynamic-fields-on-relationship/dynamic-fields-on-relationship.js you see that in order to get the image field to render I had to basically do the same thing that ACF does when an image is added to the field, here’s the bit of code that does that from that file.

    
    if (json['image']) {
    	// data-key == field key of image field
    	// put the id value into the hidden field
    	$('[data-key="field_57d9e92159b78"] input[type="hidden"]').val(json['image']['id']);
    	// put the url into the img element
    	$('[data-key="field_57d9e92159b78"] img').attr('src', json['image']['url']);
    	// set the image field to show 
    	$('[data-key="field_57d9e92159b78"]').find('.acf-image-uploader').addClass('has-value');
    }
    

    As I said, I’m working on a JS api form myself that will let me pass in an ACF field object to get and set values. Image is one of the field types that I’ve completed and this is the image field setting code from that work. If you look at the place where the image is set, it’s basically doing the same thing, because I could not figure out how to trigger ACF to re-render the image field. ACF does the same thing that I’m doing here from the value returned by the WP Media Modal. More information on what ACF is doing below.

    
    setImage: function(object, value, args) {
      // setting images are a little more work
      if (typeof(value) == 'object' && typeof(value.id) != 'undefined') {
        value = value.id;
      }
      value = parseInt(value);
      var uploader = object.closest('.acf-image-uploader');
      if (!value) {
        // clear the image
        this.setInput(object, '', args);
        object.closest('.acf-input').find('img').attr('src', '');
        if (uploader.hasClass('has-value')) {
          uploader.removeClass('has-value');
        }
        object.trigger('change');
        return;
      }
      // ajax request
      var self = this,
          data = this.o;
      data.action= 'acfField_load_image';
      data.attachment = value;
      data.size = uploader.data('preview_size');
      data.exists = [];
      
      this.request = $.ajax({
        url: acf.get('ajaxurl'),
        data: acf.prepare_for_ajax(data),
        type: 'get',
        dataType: "json",
        async: true,
        success: function(json) {
          if (!json) {
            self.setInput(object, '', args);
            object.closest('.acf-input').find('img').attr('src', '');
            if (uploader.hasClass('has-value')) {
              uploader.removeClass('has-value');
            }
            object.trigger('change');
            return;
          }
          self.setInput(object, json.id, args);
          object.closest('.acf-input').find('img').attr('src', json.src);
          if (!uploader.hasClass('has-value')) {
            uploader.addClass('has-value');
          }
          object.trigger('change');
        },
        error: function(jqXHR, textStatus, error) {
          console.log(jqXHR+' : '+textStatus+' : '+error);
        }
      });
      
      //this.setInput(object, value, args);
      //object.trigger('initialize');
    }, // end setImage
    

    If you want to see what ACF is doing, look in /advanced-custom-fields-pro/assets/js/acf-input.js

    On line ~6418 is the function add, in this function the WP media modal is opened.

    When the modal is closed ACF sets the values of the image field and the src of the image element if they are returned and then calls the render function on line ~6361, and this function basically toggles the ‘has-value’ class on the image container.

    As you can see, there isn’t any “re-rendering”. For the image field, if the field already has an image all of the needed values for the image field are set when the field is output by PHP.

    TL;DR
    If you find a better way of doing this I would really like to hear about it.

  • Thank you very much. I’m doing a similar thing at the moment:
    – I use ACF hooks in order to run callback when an image field value is changed
    – Then I read the image field data,
    – Then I read the EXIF data from the image field,
    – Then I update other fields values based on the EXIF data.

    So, as long as it’s just a matter of updating existing fields, it’s not a big problem. But if I wanted to create some taxonomy fields on the fly, then it would be tricky, because there’s no way to re-render a field and/or a field group.

    I will probably end up with writing my own ACF custom field that will handle taxonomies update. This will be a simpler and a cleaner solution because I want to have as little DOM traversing in my code as possible. I don’t want my code to break if ACF HTML structure will change.

    Thank you for your help anyway! I hope that in future ACF will be more flexible in this area. I would love to see a real, non-DOM-dependant JS API. I would love to be able to render ACF fields anywhere, with a custom HTML templates, etc. But I know how much work it requires. At the moment this part of ACF code is very monolithic and DOM-oriented. I dream of a MVC-like architecture where models and collections are separated from the view layer (like Backbone Models/Collection/Views).

    If it was implemented then ACF could become something more than just a powerful custom fields manager. If ACF has such an API and architecture then It would be even possible to build something like a page builder (like visual composer) on top of ACF!

    Sorry for the offtopic 🙂

    This is a piece of code I wrote:

    
    import $ from 'jquery';
    
    export default () => {
    
        const $$ = {
            template: $('#js-exif-data')
        };
    
        const nonce = $$.template.data('nonce');
    
        // ACF fields identifiers
        const imageInputName = 'acf[field_56d99d7de115e]';
        const orientationInputName = 'acf[field_56d8b43f73de1]';
    
        const allowedExifData = {
            created_timestamp: 'Data',
            iso: 'ISO',
            focal_length: 'Ogniskowa',
            shutter_speed: 'Migawka',
            aperture: 'Przysłona',
            keywords: 'Tagi'
        };
    
        const utils = {
            /**
             * @param {int} exifValue
             * @returns {string} - <code>landscape</code> or <code>portrait</code>
             */
            getOrientationByExifValue(exifValue) {
                const orientations = [{
                    name: 'landscape',
                    values: [1, 2, 3, 4]
                }, {
                    name: 'portrait',
                    values: [5, 6, 7, 8]
                }];
    
                const orientation = orientations
                    .filter(orientation => {
                        return orientation.values.indexOf(exifValue) !== -1;
                    });
    
                return orientation.length === 0 ? 'unknown' : orientation[0].name;
            },
            /**
             * @param {int} attachmentId
             * @returns {Promise}
             */
            loadExifData(attachmentId) {
                return $.ajax({
                    type: 'post',
                    url: acf.o.ajaxurl,
                    data: {
                        nonce,
                        attachmentId,
                        action: 'get_image_meta',
                    }
                });
            },
            /**
             * @returns {Promise}
             */
            getOrientationTerms() {
                return $.ajax({
                    type: 'post',
                    url: acf.o.ajaxurl,
                    data: {
                        nonce,
                        action: 'get_orientation_terms',
                    }
                });
            },
            /**
             * @param {object} response
             */
            renderExifData(response) {
                $.each(response.data, (k, v) => {
    
                    if (!allowedExifData.hasOwnProperty(k)) {
                        return true;
                    }
    
                    // Convert array values to coma-separated string.
                    const value = (function () {
                        if (v.constructor === Array) {
                            return v.join(', ');
                        }
                        return v;
                    }());
    
                    const $p = $('<li />').html(<code><strong>${allowedExifData[k]}</strong>: ${value}</code>);
                    $$.template.append($p);
    
                });
            },
            /**
             * @param {object} response
             */
            setOrientation(response) {
    
                if (!response.data.hasOwnProperty('orientation')) {
                    return;
                }
    
                utils.getOrientationTerms().done(termsResponse => {
    
                    const wpTerms = termsResponse.data;
    
                    if (wpTerms.constructor !== Array) {
                        return;
                    }
    
                    const exifOrientation = parseInt(response.data.orientation, 10);
                    const orientationHumanReadable = utils.getOrientationByExifValue(exifOrientation);
                    const wpTerm = wpTerms.filter(term => {
                        return term.name === orientationHumanReadable;
                    });
    
                    if (wpTerm.length === 0) {
                        return;
                    }
    
                    const orientationTermId = parseInt(wpTerm[0].id, 10);
                    const $radios = $(<code>input[name=&quot;${orientationInputName}&quot;]</code>);
    
                    const $radio = $radios.filter((index, el) => {
                        return parseInt($(el).val(), 10) === orientationTermId;
                    });
    
                    $radio
                        .prop('checked', true)
                        .trigger('click')
                });
            }
        };
    
        const handlers = {
            imageOnLoad() {
                const $input = $(<code>input[name=&quot;${imageInputName}&quot;]</code>);
                const attachmentId = $input.val();
    
                if ($input.length === 0 || attachmentId === '') {
                    return;
                }
    
                $$.template.empty();
                utils.loadExifData(attachmentId).done(utils.renderExifData);
            },
            /**
             * @param {object} $input - an array of jQuery DOM objects
             */
            imageInputOnChange($input) {
                const attachmentId = $input.val();
    
                if ($input.context.name !== imageInputName) {
                    return;
                }
    
                $$.template.empty();
    
                if (attachmentId === '') {
                    return;
                }
    
                utils.loadExifData(attachmentId).done(response => {
                    utils.renderExifData(response);
                    utils.setOrientation(response);
                })
    
            }
        };
    
        acf.add_action('change', handlers.imageInputOnChange);
        acf.add_action('load', handlers.imageOnLoad);
    }
    
  • The main problem I’ve found with not traversing the DOM is when it comes to sub fields in repeaters (or flex fields). The only unique identifier there is, is the field key that is stored in ‘data-key’ for a particular field. If you want to get or set a value in a particular row of of a sub field then you need to know the actual field object that needs to be modified. This was extremely important when I was creating a custom ACF field type because I had to know precisely what instance of a field was being added or edited if the field was a sub field.

    Let’s just say that I wanted to use JS to insert a row just before the current row that’s being edited and then insert data into the fields of the new row.

    
    var $row = e.$el.closest('.acf-row');
    $row.find('[data-event="add-row"]').trigger('click');
    var $new_row = $row.prev();
    

    The first line is exactly how ACF finds the current row.

    Now I can find/set each of the fields in the new row by using $new_row.find('[data-key="field_XYZ"] .....

    I could be wrong, but I seriously doubt that there will be extensive changes in the HTML structure of ACF simply because everything in the ACF’s JS files is highly dependent on that structure. A change in structure would likely mean a major rewrite of all the ACF JS code and that would likely mean ACF6. Even then I don’t see it happening.

    Although I am careful to use things that ACF uses, like .acf-row and [data-key...], basically the same things that ACF uses that I don’t think will change, and doing things the same basic way that ACF does them.

  • We made an offtopic anyway, so let me continue because this discussion is quite interesting.

    A picture A code sample is worth a thousand words so let me just paste a link to ACF competitor’s (maybe not yet, but who knows…) site https://carbonfields.net/docs/advanced-topics-templates/ and this: https://github.com/htmlburger/carbon-field-template

    This is awesome! I would love to see such flexibility in ACF6.

  • I wouldn’t consider it a competitor unless they plan to create an admin interface for creating fields, and it looks a lot more complicated to use, but that’s just an initial reaction after skimming the docs.

    I can agree though that a more robust JS API that allows us to easily set and get values from ACF fields as well has add actions to specific fields would be extremely useful.

Viewing 8 posts - 1 through 8 (of 8 total)

The topic ‘Does ACF have a JS API?’ is closed to new replies.