Skip to content

Instantly share code, notes, and snippets.

@juanmf
Last active August 29, 2015 13:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save juanmf/10483041 to your computer and use it in GitHub Desktop.
Save juanmf/10483041 to your computer and use it in GitHub Desktop.
/**
* Recursive Prototype Clonner:
* it relies on the following data structure:
* <someAncestor data-allow-add="true"> ...
* <button class="" data-add="add"
* data-add-prototype-node="JQ selector relative to $button.closest('[data-allow-add]'). Will be parameter of $obj.find($selector)"
* data-add-prototype-name="<?php echo $prototypeName; ?> optional, defauts to __name__"
* data-add-append-node="%data-add-prototype-node% | JQ selector relative to $button.closest('[data-allow-add]').
* Will be parameter of $obj.find($selector)"
* data-add-wrapper="html to wrap the cloned item before appending e.g. <li>__PROTOTYPE-CONTENT__</li>"
* > ...
* </someAncestor>
* e.g.
* <button class="" data-add="add"
data-add-prototype-node="ul.preciosTierra"
data-add-append-node="ul.preciosTierra"
data-add-wrapper="<?php echo htmlentities(
'<li data-allow-add="true">'
. $botonesDepto
. '__PROTOTYPE-CONTENT__'
. '</li>'); ?>"
>Crear Nueva Area</button>
*/
/** from http://stackoverflow.com/questions/322912/jquery-to-find-all-previous-elements-that-match-an-expression */
// this is a small helper extension i stole from
// http://www.texotela.co.uk/code/jquery/reverse/
// it merely reverses the order of a jQuery set.
$.fn.reverse = function() {
return this.pushStack(this.get().reverse(), arguments);
};
// create two new functions: prevALL and nextALL. they're very similar, hence this style.
$.each( ['prev', 'next'], function(unusedIndex, name) {
$.fn[ name + 'ALL' ] = function(matchExpr) {
// get all the elements in the body, including the body.
var $all = $('body').find('*').andSelf();
// slice the $all object according to which way we're looking
$all = (name == 'prev')
? $all.slice(0, $all.index(this)).reverse()
: $all.slice($all.index(this) + 1)
;
// filter the matches if specified
if (matchExpr) $all = $all.filter(matchExpr);
return $all;
};
});
/***End from */
docdigital = (typeof docdigital === "undefined") ? {} : docdigital;
$.extend(true, docdigital, {
constants: {
mediator: {
messages: {
clonePrototype_prototypeAdded: 'clonePrototype_prototypeAdded'
}
}
}
});
$.extend(true, docdigital, {
ColonePrototype: function () {
this.bindDelete = function () {
$('[data-allow-add] button[data-delete]').attr('onclick','').unbind('click');
$('[data-allow-add] button[data-delete]')
.click(
function(event) {
var $button = $(event.target);
var $container = $button.closest('[data-allow-add]');
var $prototypeNode = $container.find(($button.attr('data-add-prototype-node')));
var $appendTarget = $button.attr('data-add-append-node') === '%data-add-prototype-node%'
? $prototypeNode
: $container.find($button.attr('data-add-append-node'));
$appendTarget.each(function() {
var $toDelete = $(this).children().last();
$toDelete.remove();
});
// child Counter update
parseInt($button.attr('data-count')) > 0
&& $button.attr(
'data-count',
parseInt($button.attr('data-count')) - 1
);
return false;
}
);
};
this.bindAdd = function () {
$('[data-allow-add] button[data-add]').attr('onclick','').unbind('click');
$('[data-allow-add] button[data-add]')
.click(
function(event) {
var $button = $(event.target);
var $container = $button.closest('[data-allow-add]');
var $prototypeNode = $container.find(($button.attr('data-add-prototype-node')));
var $prototypeName = $button.attr('data-add-prototype-name') || '__name__';
var $appendTarget = $button.attr('data-add-append-node') === '%data-add-prototype-node%'
? $prototypeNode
: $container.find($button.attr('data-add-append-node'));
var $wrappingHtml = $button.attr('data-add-wrapper') && $button.attr('data-add-wrapper').length > 0
? $button.attr('data-add-wrapper')
: false;
$button.attr('data-count')
|| $button.attr(
'data-count',
$appendTarget.first().children().length
);
var nameRegex = new RegExp($prototypeName, 'g');
var labelRegex = new RegExp($prototypeName + 'label__', 'g');
$appendTarget.each(function(idx, element) {
var $clone = $prototypeNode.eq(idx).attr('data-prototype');
$clone = $clone.replace(labelRegex, $button.attr('data-count'));
$clone = $clone.replace(nameRegex, $button.attr('data-count'));
if ($wrappingHtml) {
$clone = $wrappingHtml.replace(/__PROTOTYPE-CONTENT__/g, $clone)
}
$clone = $.parseHTML($clone);
$(this).append($clone);
docdigital.mediatorInstance.send(
docdigital.constants.mediator.messages.clonePrototype_prototypeAdded,
$clone
);
});
$button.attr('data-count', parseInt($button.attr('data-count')) + 1);
var c = new docdigital.ColonePrototype();
$('li[data-allow-add] > div').each(c.tabIndxFn);
c.bindAdd();
c.bindDelete();
return false;
}
);
};
}
});
$(document).ready(
function () {
var clonner = new docdigital.ColonePrototype();
clonner.bindAdd();
clonner.bindDelete();
}
);
@juanmf
Copy link
Author

juanmf commented Apr 11, 2014

docdigital.mediatorInstance.send() is here https://gist.github.com/juanmf/10475007

@juanmf
Copy link
Author

juanmf commented Apr 11, 2014

this works with the following markup:

<script type="text/javascript" src="{{ asset('bundles/ddtools/js/clonePrototypes.js') }}"></script>

{# 
 * Renders a pair of buttons, add and delete.
 * @param string   class       The class used by jQuery script to find relevant nodes.
 * @param string   title       Printed <h3> Title </h3> for this buttons
 * @param FormView form        The rendering form 
 * @param string   widgets     The widget that is being rendered, should be a collectionType.
 * @param string   widgetBlock Optional. The block that knows the widget structure and renders it with custom markup.
 *
 #}
<div class="side_by_side" data-allow-add="true">
    <h3>{{ title }}</h3>
    <span class="buttons show control-group">
        <button class="btn" data-add="add" 
                data-add-prototype-node="ul.{{ class }}" 
                data-add-append-node="ul.{{ class }}"
                data-add-wrapper="{{ '<li class="clearfix panel panel-default container-fluid">__PROTOTYPE-CONTENT__</li>' | e }}"
            >Crear Nuevo</button>
        <button class="btn" data-delete="delete"
                data-add-prototype-node="ul.{{ class }}" 
                data-add-append-node="ul.{{ class }}">Borrar &Uacute;ltimo</button>
    </span>
    <ul class="{{ class }}"
        data-prototype="{{ form_widget(widgets.vars.prototype) | e }}">
    {% for widget in widgets %}
        <li class="clearfix panel panel-default container-fluid">
            {% if widgetBlock is defined %}
                {% include widgetBlock with { 'widget': widget } %}
            {% else %}
                {{ form_widget(widget) }}
            {% endif %}
        </li>
    {% endfor %}
    {% do widgets.setRendered %}
    </ul>
</div>

Which gets included like this:

{% include 'DdToolsBundle:Common:allowAddWidget.html.twig' 
    with { 'widgets': edit_form.type, 'class': 'classForSelectors', 'title': 'Fields'} %}

@dotstormz
Copy link

Can you give an example of this code working with multiple nested collections?

@ahmed91w
Copy link

ahmed91w commented Feb 7, 2015

Hello i have the same problem with multiple nested collections . can anybody help me

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment