0

I've tried searching for this using various terminology, but I always end up at questions pertaining to the php template files. That's not what I'm looking for, so I'm asking a new question. I apologize if this is a duplicate.

Basically, I'm looking for a way to create template posts. I'm using ACF Pro on a custom "Job" post type, and I've got a Repeater Field set up for "Job Steps."

To give an example, a "Make a Pizza" Job would have the following steps:

  1. Ordered
  2. Prep
  3. Bake
  4. Box
  5. Deliver

The way things are currently set up, users will need to manually define these steps each and every time, so what I would like to do is create a "Pizza Template" post that users can use to define the steps once, and then adding a new "Make a Pizza" job will automatically define the steps but allows users to fill in the specific details like toppings, crust, delivery address, etc.

One of the things I thought about doing was adding a new "Job Template" post type that uses the same custom field definition as the "Job" type. Then, when users create a new Job, they would be prompted to pick a "Job Template".

If they chose "Pizza Template", it would copy the Repeater Field values from the Job Template post and set them in the editor for the new Job. I suspect that would be a very convoluted process, so I'm hoping that someone knows a more efficient way somewhere in the core.

Edit

Based on comments, I see that one aspect of this question wasn't as clear as I thought, so to give more info:

I'm trying to create a generic Tracker plugin that can be customized by users at runtime to fit a variety of workflows. The Repeater Field gives me that flexibility, but efficient use depends on templates. Users define their workflow once as a template and then use that template when creating new Jobs.

This approach also allows users to define multiple workflows without the need for multiple field groups or multiple post types.

I'm working on a few possible solutions myself (and will post answers when I get there), but I'm really hoping there's something in the core that I can leverage to make this easier.

2
  • 1
    ACF is a great plugin but you may have better results defining your custom fields in a metabox with code, rather than using a repeater field. This will overcome the issue of users having to define the steps.
    – jdm2112
    Commented Nov 30, 2015 at 21:25
  • Normally I'd agree with you, but in this case, I want users to be able to define steps once. The goal is to create a generic tracker plugin that can be customized at run-time to fit a variety of workflows, which is what makes the repeater field a good fit. If I can figure out how to template, I can have one code base that accommodates printing jobs, home improvements, pizzas, etc. making fixes and updates much easier to apply across multiple sites/implementations.
    – Mike
    Commented Nov 30, 2015 at 22:03

3 Answers 3

1

It's possible you may have to define your own ACF Custom Field Type which holds your templates. And after a template is selected then you would programmatically copy the fields to a repeater. Awesome ACF has quite a few examples, WordPress has quite a few plugins and there is a User Submitted Add-Ons support forum.

2
  • Thanks for the links. At a glance, there are a handful of options there that at least look like promising starting points. I'll have to take a closer look.
    – Mike
    Commented Nov 30, 2015 at 22:42
  • The Nav Menu Field gave me the original idea.
    – jgraup
    Commented Nov 30, 2015 at 22:51
0

I was able to pull this off with a second post type and a little bit of JavaScript.

Step 1: Assign the ACF Field Group

I already had the Job Steps field group defined, so I modified the setup a bit and assigned it to both Jobs and Job Archetypes:

// [...] This is a snip from the full custom field setup
'location' => array (
    array (
        array (
            'param' => 'post_type',
            'operator' => '==',
            'value' => 'my-job',
        ),
    ),

    array (
        array (
            'param' => 'post_type',
            'operator' => '==',
            'value' => 'my-job-archetype',
        ),
    ),
),

Now I can access Job Steps in the same way for poth content types.


Step 2: Register a Custom Query Var

The next thing I did was add a custom query variable for the Archetype post ID:

function my_registerArchetypeQueryVar($vars) {
    $vars[] = 'archetypeId;
    return $vars;
}
add_filter('query_vars', 'my_registerArchetypeQueryVar');

Step 3: Front End Form - Edit Archetype

Next, I created an Edit Archetype page with the following:

if(get_query_var('archetypeId')) {
    $id = get_query_var('archetypeId');

    $jobArchetype = get_post($id);

    if ($jobArchetype) {
        acf_form(array(
            "id" => "editJobForm",
            "post_id" => $id,
            "post_title" => true,
            'field_groups' => array('my_jobStepFields'),
            "return" => "%post_url%",
            "submit_value" => __("Update Archetype", 'acf'),
        ));
    } else {
        acf_form(array(
            "id" => "addJobForm",
            "post_id" => 'new_post',
            'new_post' => array(
                'post_type' => 'my-job-archetype',
                'post_status' => 'publish'
            ),
            "post_title" => true,
            'field_groups' => array('my_jobStepFields'),
            "return" => "%post_url%",
            "submit_value" => __("Add Archetype", 'acf'),
        ));
    }
}

On the this page, I grab the archetypeId query var and use that to pull up an ACF form for the specified post. (And if no archetypeId is provided, I render a New Archetype form instead.


Step 4: Front End Form - New Job

Next, I created a New Job page with the following:

$id = get_query_var('archetypeId');
$jobArchetype = get_post($id);


if ($jobArchetype) { ?>
    <script>
        $(document).ready(function() {
            my_loadJobArchetype();
        });

        function my_loadJobArchetype() {
            $('<div>').load("<?php echo get_page_by_path('edit-archetype') 
                . "?archetypeId=" . $jobArchetype->ID; ?>" 
                + ' ' + "[data-name='wf_job_steps']", function() {

                    $("[data-name='wf_job_steps']").replaceWith(
                        $(this).children("[data-name='wf_job_steps']")
                    );
            });
        }
    </script>
<?php }

When the New Job page receives an archetype ID, it loads a custom script that essentially replaces the blank Job Steps section of the New Job form with a copy of the Job Steps section from the Edit Archetype form.


The Script

When the page is rendered in the client's browser, it will look something like this:

<script>
    $(document).ready(function() {
        my_loadJobArchetype();
    });

    function my_loadJobArchetype() {
        $('<div>').load("mysite.com/edit-archetype/?archetypeId=123" 
            + ' ' + "[data-name='my_job_steps']", function() {

                $("[data-name='my_job_steps']").replaceWith(
                    $(this).children("[data-name='my_job_steps']")
                );
        });
    }
</script>

And here's a breakdown of what's happening:

$(ducument).ready() calls my_loadJobArchetype() as soon as the page is loaded.

Next, we load a portion of the Edit Archetype page into a new <div> element with:

$('<div>').load("mysite.com/edit-archetype/?archetypeId=123" 
    + ' ' + "[data-name='my_job_steps']" // ...
);

We're actually passing something similar to two arguments here. The url argument of jQuery's load() function can also contain a jQuery selector string (separated by a space) which will load only a fragment of the target page rather than the entire document.

I inspected the source of my Edit Archetype and New Job pages, and found that the Job Step field group was contained in a <div> tag with the attribute: data-name="my-job-steps" (the value of this attribute happens to be the KEY I defined for the custom field group).

Knowing that, I built a jQuery selector based on that attribute: [data-name='my_job_steps'] which I passed in with the url argument of the load() call.

Then, in the callback function, I grab the existing Job Steps section of the page, and replace it with the section I loaded from the Edit Template page.

function() {
    $("[data-name='my_job_steps']").replaceWith(
        $(this).children("[data-name='my_job_steps']")
    );
}

The $(this) element in this case is the new <div> element we created to store the response from load().

The end result is a functional templating feature. I'm not exactly happy with the fact that I'm now relying on JavaScript for this to work, but it's a start.

When I figure out how to achieve this without JS, I'll be sure to share.

2
  • If it works, it works. Nice 'job' 8-)
    – jgraup
    Commented Dec 10, 2015 at 5:44
  • @jgraup, turns out it doesn't quite work as well as I thought. A bug surfaced pretty quickly when people started using it. -- That said, I kept digging into ACF, and it turns out there's a filter in the plugin to let me do exactly what I need. I posted that solution if you're curious.
    – Mike
    Commented Jan 8, 2016 at 4:15
0

I know I posted an answer to this already, but I've found a better solution, so I wanted to take the time to share that.

As it turns out, there is one small hiccup with the JS solution in my previous answer that didn't surface until I had a friend test my in progress application.

The Problem

Previously, I defined Job Steps as a Repeater Field with two sub-fields: Title and Details. The Details sub-field is a WYSIWYG field to give users some control over formatting.

The problem is, tinyMCE uses JavaScript that wraps plain textarea inputs in the user-friendly WYSIWYG textarea after a page has loaded. This caused problems, because my JS solution:

function my_loadJobArchetype() {
    $('<div>').load("mysite.com/edit-archetype/?archetypeId=123" 
        + ' ' + "[data-name='my_job_steps']", function() {

            $("[data-name='my_job_steps']").replaceWith(
                $(this).children("[data-name='my_job_steps']")
            );
    });
}

Didn't execute scripts during the load() call. When I replaced the blank Job content with the content loaded from the archetype, my users were getting a plain text box rather than a WYSIWYG editor.

So, instead of seeing things like Quantity, they saw:

<p><strong>Quantity</strong></p>

Needless to say, that's not what I wanted to happen.

I followed jgraup's suggestion and looked into some of the ACF plugins, but it turns out I can solve it with the filters already provided by the plugin.

The filter we want to use for this is: acf/load_value/key=[field_key]. Basically, this is a dynamic filter that lets you hook into the LOADING process for specific custom fields.


Step 1

Redefine my custom fields (slightly).

Originally, Jobs and Job Archetypes used the same field group: wf_jobStepFields, but for this process to work correctly, I need to maintain two copies of the fields:

  • wf_jobStepFields
  • wf_archetypeStepFields

Within each of those field groups is a unique repeater field: wf_job_steps and wf_archetype_steps. The sub-field keys remain the same (step_title and step_details)

Step 2

Hook into the ACF filter

function my_loadArchetypeValue($value, $post_id, $field) {

}
add_filter('acf/load_value/key=wf_job_steps', 'my_loadArchetypeValue', 10, 3);

"acf/load_value/key={$field_key}" is a flexible filter hook that allows us to latch onto the loading process for an individual field. In this case, we're latching on to the wf_job_steps Repeater Field

Step 3

Override values on the New Job Form

I've got a page on the front-end that users can visit to submit new Jobs. By default, this page contains a blank job with a single Job Step defined, but using this hook, we can override that with the selected Archetype template:

 function my_loadArchetypeValue($value, $post_id, $field) {
    // FIRST:    Check to make sure we are on the NEW JOB page.
    if (is_page('new-job')) {

        // Check to see if the "archetypeId" query variable is defined.
        if (get_query_var("archetypeId")) {
            // If it is, grab the VALUE of the ARCHETYPE STEPS for the selected Archetype
            return get_field('wf_archetype_steps',get_query_var("archetypeId"));
        }
    }
}

Step 4

Override values on the Admin screen

Although I have a front-end form for adding new Jobs, I want to make sure that the backend screen provides the same functionality so that Archetypes can be leveraged from the admin area as well.

// First, check to make sure that the get_current_screen() function is defined.
// It is not defined on every admin page, but it is defined on the 
// ADD NEW pages, which is where we want to be.
if (function_exists("get_current_screen")) {
    // Grab the current screen OBJECT and save it
    $screen = get_current_screen();

    // This statement checks to make sure that we are on the 
    // ADD NEW screen for the JOB post type
    if ($screen->action == 'add' && $screen->post_type == wf-job) {
        $archetypeId = $_GET["archetypeId"];
        /* NOTE: the global WP_Query object has no query vars here,
        /        so we need to get a little "creative to pull the
        /        variable from the URL.

           Technically, $_GET["archetypeId"] will also work on the
           Add Job form as well, but we have access to the WP_Query
           query vars there, and I think that using the WP core whenever
           possible makes WordPress related functions easier to follow
        */

        if ($archetypeId) {
            return get_field('wf_archetype_steps', $archetypeId);
        }
    }
}

The Final Result

Put it all together, and the final function looks like this:

function my_loadArchetypeValue($value, $post_id, $field) {
    if (is_page('new-job')) {
        $archetypeId = get_query_var("archetypeId");

        if ($archetypeId) {
            return get_field('wf_archetype_steps',$archetypeId);
        }
    } elseif (function_exists("get_current_screen")) {
        $screen = get_current_screen();

        if ($screen->action == 'add' && $screen->post_type == wf-job) {
            $archetypeId = $_GET["archetypeId"];
            if ($archetypeId) {
                return get_field('wf_archetype_steps', $archetypeId);
            }
        }
    }

    return $value;
}
add_filter('acf/load_value/key=wf_job_steps', 'my_loadArchetypeValue', 10, 3);

NOTE: As a rule, I'm not a huge fan of defining a second field group. In this case, it's pretty much a textbook definition of copy/pasting code, since the field groups are functionally identical aside from a couple keys.

But, there is a very good reason for doing it in this case.

The filter we hook into latches on to the LOAD event for a specific field (defined by the field's KEY). In this case, we're latching onto wf_job_steps.

If I had continued to use a single field group, this filter hook would throw the process into an infinite loop, because each time my filter function hit the get_field() call, the filter would be triggered again.

Defining a second field group with a unique KEY for the repeater field prevents the infinite loop, but since the sub-fields still have the same KEY values, it allows them to transfer seamlessly over to new JOBS.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.