grapesjs: Question: Dragging blocks into a Text block?

This is not a bug but an implementation question. If this is not the right place to ask these questions, please let me know.

We’re working on an implementation where we want to use GrapesJS to allow users to create an e-mail template. As part of this implementation we are working to create mail-merge functionality: we’ve introduced the concept op ‘merge-fields’ or ‘placeholders’ which we will replace with the proper values on the server side. This means we send over the components JSON structure and turn it into HTML server-side, replacing values as we go.

So, as an example, one of our users might enter the text: Hello <<username>> and we’ll replace that merge-field <<username>> with the proper field.

But, we haven’t been able to implement this this way quite yet as we’re not able to drag these blocks into a Text block. We can only drag it around it. So, for now we’re extending the RTE with a merge-field ‘inline block’ ( <input type=text readonly class=mergeField data-isMergefield=1 />) and creating a merge-field block with the same HTML in the block manager. Implementing a DomComponent type to recognize it offers a method to configure it. But it feels suboptimal, we’d really like to be able to drag that mergefield block onto the right place in the textfield.

To allow this, I imagine GrapesJS would have to be able to grab the textNode and split it in to (atleast) two textnodes and a tag for the merge-field but I’m not sure where to start with this. Could you advise as to how we might implement this?

About this issue

  • Original URL
  • State: closed
  • Created 7 years ago
  • Comments: 42 (13 by maintainers)

Most upvoted comments

Have you checked API-Rich-Text-Editor? You can add a custom action like this

editor.RichTextEditor.add('custom-vars', {
  icon: `<select class="gjs-field">
		<option value="">- Select -</option>
        <option value="[[firstname]]">FirstName</option>
        <option value="[[lastname]]">LastName</option>
        <option value="[[age]]">Age</option>
      </select>`,
    // Bind the 'result' on 'change' listener
  event: 'change',
  result: (rte, action) => rte.insertHTML(action.btn.firstChild.value),
  // Reset the select on change
  update: (rte, action) => { action.btn.firstChild.value = "";}
})

rte-action

Probably in the next release, this feature will be available. textable

So textable will be just another property, this will allow any component to be dropped inside Text components. Here is the code of the component from the example above:

// Define a component with `textable` property
editor.DomComponents.addType('var-placeholder', {
      model: {
        defaults: {
          textable: 1,
          placeholder: 'VARIABLE-1',
        },
        toHTML() {
          return `{{ ${this.get('placeholder')} }}`;
        },
      },
	  // The view below it's just an example of creating a different UX
      view: {
        tagName: 'span',
        events: {
          'change': 'updatePlh',
        },
        // Update the model once the select is changed
        updatePlh(ev) {
          this.model.set({ placeholder: ev.target.value });
          this.updateProps();
        },
        // When we blur from a TextComponent, all its children components are
        // flattened via innerHTML and parsed by the editor. So to keep the state
        // of our props in sync with the model so we need to expose props in the HTML
        updateProps() {
          const { el, model } = this;
          el.setAttribute('data-gjs-placeholder',  model.get('placeholder'));
        },
        onRender() {
          const { model, el } = this;
          const currentPlh = model.get('placeholder');
          const select = document.createElement('select');
          const options = [ 'VARIABLE-1', 'VARIABLE-2', 'VARIABLE-3' ];
          select.innerHTML = options.map(item => `<option value="${item}" ${item === currentPlh ? 'selected' : ''}>
			${item}
		  </option>`).join('');
          while (el.firstChild) el.removeChild(el.firstChild);
          el.appendChild(select);
          select.setAttribute('style', 'padding: 5px; border-radius: 3px; border: none; -webkit-appearance: none;');
          this.updateProps();
        },
      }
    });
	
	// Use the component in blocks
    editor.BlockManager.add('simple-block', {
      label: 'Textable block',
      content: { type: 'var-placeholder' },
    });

Yes, I’ve used that and I’ve pretty much got that working. The difference is that I am not using a text placeholder like you are. I’m actually inserting a block (with a corresponding ‘type’) so that I can further configure these placeholders (for instance, a field might be a Datetime field and my user may want to configure the exact output format for that datetime). I used HTML5 drag’n’drop to implement this and that works pretty nice, bút…

It feels like departing from the expected user interface. I feel I should be able to drag a mergefield from the ‘blocks’ and onto the right position. I’ve been toying around with the Sorter to allow this and I’m up to this:

image

I now have the problem that the Sorter very much wants actual blocks to align to so I have some more tweaking to do. For now implementing this has required changed in the ComponentTextView (mostly: dont clear out the toolbar for ‘mergefields’) and the Sorter (if I’m hovering a mergefield over a textblock, insert HTML into the activeRTE instead of appending a block).

I’m currently working on making the dragging work more reliably (for some reason, whenever I drag right the field gets appended to the textnode instead of at the cursor position, works fine when dragging left 😃 ), making the sorter ignore the idea of ‘blocks’ when within a textfield, showing the proper placeholder in that case (I’d want to see the actual cursor) and cleaning things up.

Not sure on how to approach the Sorter issue at this point, short of specialcasing textblocks so if you have any ideas in that area I’d love to hear them. You can see some of the hacky code I’ve made so far over at https://github.com/mathieuk/grapesjs/commit/d58c5ee5306c358cd19509f6b8affe9bb60493ed .

can we re-open this case @artf ?

I hope to hear more from @mathieuk about this 😃

Hello I am having the same issue of @tudor-ooo and @Joshmamroud , did you guys found any solution ?

Hi @artf First of all, thank you for all your work. I am discovering Grapesjs and it is truly awesome.

I am experiencing the same issue as @Joshmamroud and was wondering if we are doing something wrong or whether there is some known issue with this code.

Any help would be very much appreciated.

Regards

@artf Your example works when I don’t try to drop it into a text block however when I try to drop it in a text block I get this error:

grapes.min.js:2 Uncaught TypeError: Cannot read property ‘attributes’ of undefined at r.updateAttributes (grapes.min.js:2) at renderAttributes (grapes.min.js:2) at render (grapes.min.js:2) at r.move (grapes.min.js:11) at grapes.min.js:11 at Array.forEach (<anonymous>) at r.endMove (grapes.min.js:11) at Et (grapes.min.js:2) at r.<anonymous> (grapes.min.js:2) at r.endMove (grapes.min.js:2) at i (grapes.min.js:11) at t.value (grapes.min.js:11) at t.value (grapes.min.js:11) at Et (grapes.min.js:2) at HTMLDivElement.<anonymous> (grapes.min.js:2) at HTMLDivElement.<anonymous> (grapes.min.js:2)

I’ve literally copied and pasted the code from the example for both the dom component and block. I’m trying to add the placeholder-var block to the text block from the basic blocks plugin.

I don’t know where this is coming from because it’s minified and I can’t figure out how to use the unminified version of the npm package.

Any help would be greatly appreciated.

Thanks!

For those looking for a ckeditor 4 merge fields plugin, this works: https://github.com/57u To activate:

'gjs-plugin-ckeditor': {
	options: {
		...
		extraPlugins: '...,strinsert',
		toolbar: [
			...
			{name: 'Merge Fields', items: [ 'strinsert' ]},
		],
		strinsert_strings: [
			{'value': '*|FIRSTNAME|*', 'name': 'First name'},
			{'value': '*|LASTNAME|*', 'name': 'Last name'},
			{'value': '*|INVITEURL|*', 'name': 'Activate invite URL'},
		],
		// Optionally add the below settings
		strinsert_button_label: 'Merge Fields',
		strinsert_button_title: 'Insert Merge Field',
		...
	}
},

@mathieuk can I just use any block content? I mean something like this:

blockManager.add('my-block', {
     label: 'Block',
     textable: 1, 
     content: `<div style="...">custom stuff / custom components</div>`,
})

I don’t care about thecontent, I know that with textable I’m able to drag it inside text components. Would be awesome if you set up a demo just to test it online 😃 (with something like codepan/codesandbox)

@artf so, I’ve taken your advice and went that route and i now have a fairly functional situation where it lets you drag a mergefield onto a textview, lets you move it around and lets you move it between textviews and other blocks.

image

You can see my implementation over at https://github.com/mathieuk/grapesjs/tree/mergefields/src .
Would you be interested in having this in core? Would you need any additional changes for that?