Introduction to ACF JS Models

Guides Development Introduction to ACF JS Models

#ACF Models Basics

One of the main concept driving the ACF API are models. They act like a class in PHP, which can be extended and instantiated. The main acf.Model comes with a lot of handy functions and logic, which make writing Javascript more simple.

A classic acf.Model is instantiated like this:

// instantiate
var myModel = new acf.Model({

    initialize: function(){}

});

it is also possible to delay the initialization by extending a model:

// extend
var myModel = acf.Model.extend({

    initialize: function(){}

});

// instantiate
new myModel();

The initialize() method acts like the __construct() method in a PHP class. It is called whenever the model is instantiated:

var myModel = new acf.Model({

    initialize: function(){
        console.log('myModel is initialized!');
    }

});

#ACF Models Data

Models come with data getter and setter mechanics allowing to conveniently use custom values accross the model methods. Data are set in the this.data object and can be retrieved using the this.get() method.

Additionnally the this.has() method will check if a data exists, and this.set() will add/update a value.

var myModel = new acf.Model({

    data: {
        key: 'value'
    },

    initialize: function(){
        
        // output 'key: value'
        console.log('key:', this.get('key'));
        
        // set new data
        this.set('key', 'new_value');
        
        // output 'key: new_value'
        console.log('key:', this.get('key'));

    },

});

#Hooks in ACF Models

If you’re familiar with the ACF JS hooks, you know that ACF ported the WordPress add_action & add_filter mechanics into its JS API.

For example, here is a way to hook in the $(document).ready() using ACF hooks:

acf.addAction('ready', function(){
    console.log('document is ready');
});

The ACF hooks logic are also at the core of acf.Model. In fact, a model can hook in a action to call the initialize() method using the wait property.

Thereby, both codes will produce the same result:

acf.addAction('ready', function(){
    console.log('document is ready');
});

var myModel = new acf.Model({

    wait: 'ready',

    initialize: function(){
        console.log('document is ready');
    }

});

Additionally, it is possible to define the actions and filters objects to run multiple hooks independently from the initialize() constructor.

var myModel = new acf.Model({

    actions: {
        'prepare': 'onPrepare',
        'ready':   'onReady',
    },

    filters: {
        'validation_complete': 'onValidationComplete',
    },

    initialize: function(){
        console.log('myModel is instantiated');
    },

    onPrepare: function(){
        console.log('document is prepared');
    },

    onReady: function(){
        console.log('document is ready');
    },

    onValidationComplete: function(data, $form, instance){
        console.log('validation is complete');
        return data;
    }

});

The actions and filters objects use the internal method this.addActions() and this.addFilters(), which can be called individually too:

var myModel = new acf.Model({
    
    initialize: function(){

        console.log('myModel is instantiated');
        
        this.addActions({
            'prepare': 'onPrepare',
            'ready':   'onReady',
        });
        
    },

    onPrepare: function(){
        console.log('document is prepared');
    },

    onReady: function(){
        console.log('document is ready');
    },

});

#Extending a Model

One powerful feature of ACF models is the ability to extend it, just like a PHP class. By default, the acf.Model.extend() call will overwrite any top level property, object or method with passed properties.

// create myModel
var myModel = acf.Model.extend({

    id: 'myModel',

    initialize: function(){
        console.log('model instantiated id:', this.id);
    },

});

// ouput 'model instantiated id: myModel'
new myModel();

// extend myModel
var anotherModel = myModel.extend({

    // overwrite 'id'
    id: 'anotherModel'
    
});

// ouput 'model instantiated id: anotherModel'
new anotherModel();

It is also possible directly extend and instanciate anotherModel:

// directly instantiate myModel
// ouput 'model instantiated id: anotherModel'
var anotherModel = new myModel({

    // overwrite 'id'
    id: 'anotherModel'
    
});

The extending logic is defined inside the internal method called setup(), available in every acf.Model. This method is called before the initialize(). Here is the default setup() source code:

// extend
var myModel = acf.Model.extend({

    id: 'myModel',

    setup: function(props){
        
        // overwrite toplevel property/object/method
        $.extend(this, props);
        
    },

    initialize: function(){
        console.log('model instantiated id:', this.id);
    },

});

// instantiate
new myModel();

Since it is also possible to overwrite the setup() method, we can set a different logic. In this example myModel will overwrite this.data values when extended, instead of overwriting toplevel property.

// prepare myModel
var myModel = acf.Model.extend({
    
    // default data
    data: {
        key: ''
    },

    setup: function(props){

        // overwrite data values
        $.extend(this.data, props);

    },

    initialize: function(){
        
        // log key data when instantiated
        console.log('key:', this.get('key'));
        
    },

});

// output 'key: my_value'
new myModel({
    key: 'my_value'
});

// output 'key: my_value_2'
new myModel({
    key: 'my_value_2'
});

A typical real life example taken from the ACF JS API documentation is a Person model with firstName and lastName data overwritten in each instance.

// Person
var Person = acf.Model.extend({

    data: {
        firstName: '',
        lastName: '',
    },

    setup: function(props){
        $.extend(this.data, props);
    }

});

// persons
var person1 = new Person({firstName: 'John', lastName: 'Doe'});
var person2 = new Person({firstName: 'Jane', lastName: 'West'});

// change lastName on person2
person2.set('lastName', 'Smith');

#Model Element

It is possible to tie a jquery element to a model using the $el property. Doing this will attach the model instance to the element, allowing to retrieve it  using acf.getinstance() . It will also unlock new methods in the model.

// spawner
new acf.Model({
    wait: 'prepare', // attach action to page footer (before document.ready)
    initialize: function(){
        new myModel();
    }
});

// model
var myModel = acf.Model.extend({

    setup: function(){
        this.$el = $('.my-wrapper'); // attach $el
    },

    initialize: function(){

        // find within $el
        // equivalent to $('.my-wrapper').find('.my-element')
        var search = this.$('.my-element');

        // show element
        // equivalent to $('.my-wrapper').addClass('acf-hidden')
        this.show();

        // hide element
        // equivalent to $('.my-wrapper').removeClass('acf-hidden')
        this.hide();
        
        // add event
        // equivalent to $('.my-wrapper').on('click', 'a')
        this.on('click', 'a', function(){
            console.log('Link clicked')
        });

    }

});

// retrieve model instance from html element
var $myWrapper = $('.my-wrapper');
var myModel = acf.getInstance($myWrapper);

#Model Events

An another important concept in models are events. Models events are scoped within this.$el defined in the model. If no this.$el is defined, the model fallback to $(document).

Here is a simple example to hook on a document scroll event. These scripts will all produce the same result:

// javascript method
document.addEventListener('scroll', function(){
    console.log('document scroll');
});

// jquery method
$(document).on('scroll', function(){
    console.log('document scroll');
});

// acf model method
var myModel = new acf.Model({

    events: {
        'scroll': 'onScroll',
    },

    onScroll: function(){
        console.log('document scroll');
    }

});

Since it is possible to retrieve the scroll position from $(window).scrollTop(), we will change the this.$el from the example above to $(window) and retrieve the scroll position from this.$el:

// spawner
new acf.Model({
    wait: 'prepare', // attach action to page footer (before document.ready)
    initialize: function(){
        new myModel();
    }
});

var myModel = acf.Model.extend({

    setup: function(){
        this.$el = $(window); // attach $el
    },

    events: {
        'scroll': 'onScroll',
    },

    onScroll: function(){
        console.log('window scroll with position:', this.$el.scrollTop());
    }

});

Since events are scoped within an element, it possible to target a specific sub-element. Here is a usage example:

<div class="my-wrapper">
    <a href="#">Link</a>
</div>

<a href="#">Link</a>
var myModel = new acf.Model({

    wait: 'prepare',

    initialize: function(){
        this.$el = $('.my-wrapper');
    }

    events: {
        'click a': 'onClick',
    },

    onClick: function(e, $el){
        console.log('link inside my-wrapper click', $el);
    }

});

It is also possible to add events in a method using this.addEvents() or individually with this.on(). Here is a usage example:

var myModel = new acf.Model({

    wait: 'prepare',

    initialize: function(){

        // attach $el
        this.$el = $('.my-wrapper');
        
        // add multiple events
        this.addEvents({
            'click a': 'onClick'
        });
        
        // add a single event
        this.on('click a', 'onClick');

    },

    onClick: function(e, $el){
        console.log('link click', $el);
    }

});