I am integrating the Quill editor with Rails and would like to control it with StimulusJS. Essentially I have a field on a Rails form, which has html stored in it, and it becomes and Quill rich text editor. There are also 2 other field consuming data from Quill: plain text (stripped of tags) and deltas (changes log).
All three are then saved to the db via the usual form submit.
It is almost working.
The form and controller code
<div data-controller='quill'>
<%= f.input :target_note,
input_html: {
data: {
target: 'quill.editor',
action: "keyup->quill#update" } } %>
<input data-target='quill.delta'/>
<input data-target='quill.text' />
</div>
The Stimulus controller
import { Controller } from "stimulus";
export default class extends Controller {
static targets = ['editor','delta','text'];
connect() {
console.log('connected');
this.quill = new Quill('#grammar_note_target_note', {
theme: 'snow'
});
}
update(){
console.log(this.quill.getContents());
console.log(this.quill.getText());
this.textTarget.value = this.quill.getText();
this.deltaTarget.value = this.quill.getContents();
}
}
Problem
When the user types into the editor field, the data that was previously saved is copied as delta / plain text to the textTarget and deltaTarget fields. In the image below, the value of 'asdasdasdsa' was previously saved into the field, while the user just typed 'hhh'.
Expected behaviour
On every keystroke, the actual text would be transformed into deltas or text and stored in the 2 fields.
Any help would really be appreciated. :grin:
Edit
#jhchen every keystroke produces the same, original data that was in the field. The changes are reflected on screen, but not in the 2 inputs or console.
Well I got it all working, but am not 100% sold on StimulusJS. There is possibly a better way to do this, but this is my solution:
javascript
import { Controller } from "stimulus";
export default class extends Controller {
static targets = ['editor','html','text'];
initialize() {
this.quill = new Quill(this.editorTarget, {
theme: 'snow'
});
this.quill.on("text-change", e => {
this.textChange();
});
}
textChange(){
this.htmlTarget.value = this.quill.root.innerHTML;
this.textTarget.value = this.quill.getText();
}
}
view
<div data-controller='quill'>
<div data-target='quill.editor'>
<% if f.object.target_note.present? %>
<%= f.object.target_note.html_safe %>
<% end %>
</div>
<%= f.input :target_note,
label: false,
input_html: {
class: 'display-none',
data: { target: 'quill.html' } } %>
<%= f.input :target_note_text,
label: false,
input_html: {
class: 'display-none',
data: { target: 'quill.text' }
} %>
</div>
The display-none class and label: false where needed as StimulusJS cannot target a hidden field !!!
Related
I have a weird behavior of FormGroup.
What I am doing exactly is that I am generating FormControls into the desired form group, according to number of key properties of an array.
Lets say I have the following array:
arrayData = [
{_id: 123, _name: 'XYZ', gender: 'Male'},
{_id: 124, _name: 'XYW', gender: 'Female'}
];
The extracted arra properties are as the following:
this.odkDataIndexes = ['_id', 'name', 'gender'];
Now, for each index, I need to generate a form control into the indexesForm form group:
async createIndexesForm(extractedIndexesArray) {
const controls = extractedIndexesArray.reduce((g, k) => {
g[k] = '';
return g;
}, {});
this.indexesForm = this.fb.group({controls});
console.log(this.indexesForm);
}
This function will be fired, once the user click on a button having a click event: (click)=generateMappingFields():
async generateMappingFields() {
this.showFields = false;
this.removeUnnecessaryFieldsMsg = '';
if (this.odkDataIndexes.length > 0) {
await this.createIndexesForm(this.odkDataIndexes).then(() => {
this.showFields = true;
this.tabIndex = 1;
});
}
}
For the console.log(this.indexesForm); output, the for appears to be created:
And it contains the following controls:
Which are exactly the same indexes, that I've extracted from my data.
Now I need to display these controls as following:
<div class="flexColEven">
<mat-spinner *ngIf="!indexesForm" value="50" class="setSpinnerOnTop" diameter="75" [color]="medair-color">
</mat-spinner>
<div *ngIf="showFields && indexesForm">
<div [formGroup]="indexesForm" *ngIf="indexesForm" class="flexRow">
<!-- {{indexesForm.valid | json}} -->
<div *ngFor="let controlName of odkDataIndexes">
<span></span>
<div>
<h3>ONA field: {{controlName}}</h3>
</div>
<mat-form-field class="formFieldWidth" color="warn" appearance="fill">
<mat-label>{{controlName}}</mat-label>
<mat-select [formControlName]="controlName">
<mat-option *ngFor="let de of dataElementsDetails; let i = index;"
[value]="de.id">
{{de.displayName}}
</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
</div>
</div>
When indexesForm is created, I will show the div having a drop down list for each controller, which is showing properly, but showing at the same time an error, for each controller saying:
core.js:6185 ERROR Error: Cannot find control with name: '_id'
core.js:6185 ERROR Error: Cannot find control with name: '_tag'
...
But the fields are shown properly as this stackblitz shows you.
And when I need to get the value of each drop down list:
mapping() {
this.odkDataIndexes.forEach((arrayIndexControl) => {
console.log(arrayIndexControl);
console.log(this.indexesForm.get(arrayIndexControl));
});
}
I can clearly see the arrayIndexControl which is the name of the form control of the indexesForm group, but the second console is displaying the following response:
null
And if I consoled (this.indexesForm.get(arrayIndexControl).value);, it will return an error of undefined.
Here is a stackblitz having an example of data I have.
On this line this.indexesForm = this.fb.group({controls}); you are creating a form group with a single form control in it called controls. Replace with this: this.indexesForm = this.fb.group(controls);
In general, doing const obj = {controls}; is the same as const obj = { controls: controls };
I have created an editor using Slate js in react.
I am trying to insert a block at the end of the editor content.
I came across a method to insert block at the range. How to specify the range of the document such that my custom blocks gets added at the end of the content but focus stays at the current selection and not at the end of the document.
function insertFile(editor, src, target) {
editor.insertBlock({
type: 'file',
data: { src },
})
}
My schema looks like this
const schema = {
blocks: {
file:{
isVoid: true
}
}
}
Using the Transforms object, it behaves just like you wished.
import { Transforms } from 'slate'
Transforms.insertNodes(
editor,
{ type: 'paragraph', children: [{ text: 'xxxx' }] },
{ at: [editor.children.length] }
)
To see the document for this: Link1 Link2
I have functionality where developers can add custom Angular views where they can bind properties to the $scope.settings object. When clicking on the save button the $scope.settings object will be converted to JSON and saved to the database. Something like this will be the result:
{
"name": "bob",
"age": "25"
}
As long as I add elements like <input type="text" ng-model="settings.name" /> everything goes as expected.
But, now I want to add directives like this:
<umb-property property="property in properties">
<umb-editor model="property"></umb-editor>
</umb-property>
With the following code:
$scope.properties = [
{
label: 'Name',
alias: 'name',
view: 'textbox',
value: $scope.settings.name
},
{
label: 'Age',
alias: 'age',
view: 'number',
value: $scope.settings.age
}
];
The 'editor' directive loads views in place based on the 'view' property. The views are third party. The editors are loaded in a dialog. After submission of the settings dialog, the following line of code will convert the settings to JSON:
$scope.dialog = {
submit: function (model) {
var settingsJson = JSON.stringify(model.settings);
},
close: function (oldModel) {
//
}
};
In this case I cannot parse the $scope.settings to JSON, because the $scope.settings.name is not updated anymore. The $scope.editorModel.value was updated instead.
How can I bind the $scope.editorModel.value to $scope.settings.name?
I don't want to end up with a solution where I must update all $scope.settings values with the corresponding values from the editor models, because I want to support the dynamic way to convert the $scope.settings to a JSON value in the database.
For example I dont want to do: $scope.settings.name = $scope.properties[0].value
Use property accessors:
for (var i=0; i<$scope.properties.length; i++) {
$scope.settings[$scope.properties[i].alias] = $scope.properties[i].value;
};
HTML
<div ng-repeat="prop in properties">
<input ng-model="prop.value">
</div>
I have an order line grid where I need to be able to open the popup editor form programmatically with the edit form fields pre-populated (using AngularJs).
In the HTML, I have a lineGrid and an addButton, which calls addRow() on the ticketEntryController:
<div id="wrapper" class="container-fluid" ng-controller="ticketEntryController">
<div ng-controller="ticketLineController">
<div kendo-grid="ticketLineGrid" k-options="getTicketLineGridOptions()"></div>
</div>
<button id="addButton" ng-click="addRow()" class="btn btn-primary btn-sm">Add Row</button>
</div>
Here is the ticketEntryController:
(function () {
'use strict';
angular.module('app').controller('ticketEntryController', ticketEntryController);
function ticketEntryController($scope) {
$scope.lineGrid = {};
$scope.addRow = function () {
var item = { itemNo: 'TEST123', itemDescr: 'Some description' };
$scope.$broadcast('AddRow', item);
}
}
})();
Here is part of the ticketLineController:
function ticketLineController($scope) {
$scope.$on('AddRow', function(event, item) {
console.log("ticketLineController, AddRow: " + item.itemNo);
$scope.itemNo = item.itemNo;
$scope.itemDescr = item.itemDescr;
$scope.ticketLineGrid.addRow();
});
Plunker: http://plnkr.co/edit/VG39UlTpyjeTThpTi4Gf?p=preview
When the Add Row button is clicked, the editor popup form opens up, but all fields are empty. How can I populate the fields (like they are when you click the Edit button for an existing row)?
I figured out how to get a row to be pre-populated for you, although I'm not sure if this is necessarily the best way to do it, but it does accomplish the job - I'm more familiar with AngularJs, not so much with Kendo UI.
The only place that the Kendo API allows you to change/set the new item that you are adding is in the edit event but I couldn't see a way to send your own object along to the event when you call addRow so you need to have a reference to a shared object in your controller with I called itemForAdd. Before calling addRow() in your controller, you need to set the itemForAdd object with the actual object that you want to pre-populate the form with.
var itemForAdd = {};
$scope.$on('AddRow', function(event, item) {
// save reference to item to use for pre-population
itemForAdd = item;
$scope.ticketLineGrid.addRow();
});
Now in the edit event that the Kendo API sends out, you can populate the items from your selected item in the model item. It's not really required, but I also like to clear out objects that I use like this so in the save and cancel events, I clear out the shared itemForAdd object.
edit: function (e) {
if (e.model.isNew()) {
e.model.set("itemNo", itemForAdd.itemNo);
e.model.set("itemDescr", itemForAdd.itemDescr);
}
var popupWindow = e.container.getKendoWindow();
e.container.find(".k-edit-form-container").width("auto");
popupWindow.setOptions({
width: 640
});
},
save: function(e) {
if (e.model.isNew()) {
// clear out the shared object
itemForAdd = {};
}
},
cancel: function(e) {
if (e.model.isNew()) {
// clear out the shared object
itemForAdd = {};
}
}
With the previous changes, the functionality that you want is mostly working but the data in the table in the edit popup doesn't show the updated values. This is because the Kendo data bindings apparently didn't know they had to update. I couldn't figure out how to make that work, so I just used the AngularJs style bindings for that table (where you had +=itemNo=+), so that the values in the table would update based on the changes in the model object:
<tbody>
<tr>
<td>{{dataItem.itemNo}}</td>
<td>{{dataItem.itemDescr}}</td>
<td>{{dataItem.cat}}</td>
<td>{{dataItem.mfg}}</td>
<td>{{dataItem.mfgPartNo}}</td>
</tr>
</tbody>
But there was one more issue at this point, only the itemNo was being updated, not the itemDescr and that was because itemDescr was set as editable: false in your grid configuration, so I had to changed it to editable: true
fields: {
id: { type: "string", editable: false },
itemDescr: { type: "string", editable: true },
...
},
And finally, here is an updated plunker with my changes: http://plnkr.co/edit/rWavvMh4dRFAsJjuygQX?p=preview
The following screenshot shows a combined form for sign-in and sign-up:
The following module is used to render the AuthView:
MyApp.module("User", function(User, App, Backbone, Marionette, $, _) {
User.AuthView = Marionette.ItemView.extend({
className: "reveal-modal",
template: "user/auth",
ui: {
signInForm: "#signin-form",
signUpForm: "#signup-form"
},
events: {
"focus input": "onFocus"
},
onFocus: function() {
console.log("Some input field has received focus.");
},
onRender: function() {
this.signInForm = new Backbone.Form({
schema: {
signInEmail: {
type: "Text",
title: "E-Mail address"
},
signInPassword: {
type: "Password",
title: "Password"
}
}
}).render();
this.ui.signInForm.prepend(this.signInForm.el);
this.signUpForm = new Backbone.Form({
schema: {
signUpEmail: {
type: "Text",
title: "E-Mail address"
},
signUpPassword: {
type: "Password",
title: "Password"
},
signUpPasswordConfirmation: {
type: "Password",
title: "Password confirmation"
}
}
}).render();
this.ui.signUpForm.prepend(this.signUpForm.el);
}
});
});
How can I automatically focus the first field in each sub-form whenever it is rendered? The first fields would be signInEmail for the signInForm and signUpEmail for the signUpForm.
I tried to listen to focus input events. Such an event is triggered when I click into one of the input fields, not before.
Meanwhile, inspired by the current answers I came up with the following helper function:
focusFirstFormField: function(form) {
if (_.isUndefined(form)) {
throw "IllegalStateException: Form is undefined."
}
// TODO: AuthView does not focus first sign-in field.
var firstInput = form.find(':input:first');
if (_.isObject(firstInput)) {
if (firstInput.length > 0) {
firstInput = firstInput[0];
}
else {
throw "IllegalStateException: Form find returns an empty jQuery object."
}
}
_.defer(function() {
firstInput.focus();
});
}
There is still need for improvement, though.
The events object are DOM events which are generally triggered by the user so that's not what you'll likely want to use in this case.
If I'm understanding you correctly you would like to put the focus in the first input of each of the forms but since you can only have focus on one thing at a time and they are rendering together you'll have to choose one or the other.
The simplest option is to add another line at the end of onRender focusing on the input. If your input is generating an input something like this:
<input type="text" name="signInEmail">
Then you can add:
this.$('[name=signInEmail]').focus();
If not you'll have to change the selector this.$(xxxx).focus() to suit.
You can use onDomRefresh event of the view. It will be triggered after view rendered and Dom refreshed.
onDomRefresh: function() {
this.focusFirstInput();
};
focusFirstInput: function() {
this.$(':input:visible:enabled:first').focus();
};
This solution applies to general cases. However, pay attention if you are using Bootstrap. I can't get this work there. Instead, I set autofocus: 'autofocus' in the field and it works.
You can add it to onRender method.
this.ui.signInForm.find('input[type=text]:first').focus();
this.ui.signUpForm.find('input[type=text]:first').focus();
On the onRender method I do :
$(this.el).find(':input[autofocus]').focus();
And I add the autofocus="" flag onto my HTML node. This works for refresh.