Bind Ckeditor value to model text in AngularJS and Rails - ruby-on-rails-3

I want to bind CKEditor text to ng-model text. My View:
<form name="postForm" method="POST" ng-submit="submit()" csrf-tokenized class="form-horizontal">
<fieldset>
<legend>Post to: </legend>
<div class="control-group">
<label class="control-label">Text input</label>
<div class="controls">
<div class="textarea-wrapper">
<textarea id="ck_editor" name="text" ng-model="text" class="fullwidth"></textarea>
</div>
<p class="help-block">Supporting help text</p>
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Post</button>
<button class="btn">Cancel</button>
<button class="btn" onclick="alert(ckglobal.getDate())">Cancel123</button>
</div>
</fieldset>
</form>
controller
function PostFormCtrl($scope, $element, $attrs, $transclude, $http, $rootScope) {
$scope.form = $element.find("form");
$scope.text = "";
$scope.submit = function() {
$http.post($scope.url, $scope.form.toJSON()).
success(function(data, status, headers, config) {
$rootScope.$broadcast("newpost");
$scope.form[0].reset();
});
};
$scope.alert1 = function(msg) {
var sval = $element.find("ckglobal");
//$('.jquery_ckeditor').ckeditor(ckeditor);
alert(sval);
};
}
PostFormCtrl.$inject = ["$scope", "$element", "$attrs", "$transclude", "$http", "$rootScope"];
I want to set CKEditor value in $scope.text at the time of form submit.

CKEditor does not update textarea while typing, so you need to take care of it.
Here's a directive that will make ng-model binding work with CK:
angular.module('ck', []).directive('ckEditor', function() {
return {
require: '?ngModel',
link: function(scope, elm, attr, ngModel) {
var ck = CKEDITOR.replace(elm[0]);
if (!ngModel) return;
ck.on('pasteState', function() {
scope.$apply(function() {
ngModel.$setViewValue(ck.getData());
});
});
ngModel.$render = function(value) {
ck.setData(ngModel.$viewValue);
};
}
};
});
In html, just use:
<textarea ck-editor ng-model="value"></textarea>
The previous code will update ng-model on every change.
If you only want to update binding on save, override "save" plugin, to not do anything but fire "save" event.
// modified ckeditor/plugins/save/plugin.js
CKEDITOR.plugins.registered['save'] = {
init: function(editor) {
var command = editor.addCommand('save', {
modes: {wysiwyg: 1, source: 1},
readOnly: 1,
exec: function(editor) {
editor.fire('save');
}
});
editor.ui.addButton('Save', {
label : editor.lang.save,
command : 'save'
});
}
};
And then, use this event inside the directive:
angular.module('ck', []).directive('ckEditor', function() {
return {
require: '?ngModel',
link: function(scope, elm, attr, ngModel) {
var ck = CKEDITOR.replace(elm[0]);
if (!ngModel) return;
ck.on('save', function() {
scope.$apply(function() {
ngModel.$setViewValue(ck.getData());
});
});
}
};
});

If you simply want to retrieve text in the editor textarea in angular, call CKEDITOR.instances.editor1.getData(); to get the value directly in an angularjs function. See below.
In your html
In the test.controller.js
(function () {
'use strict';
angular
.module('app')
.controller('test', test);
test.$inject = [];
function test() {
//this is to replace $scope
var vm = this;
//function definition
function postJob()
{
vm.Description = CKEDITOR.instances.editor1.getData();
alert(vm.Description);
}
}
})();

the Vojta answer work partly
in this post I found a solution
https://stackoverflow.com/a/18236359/1058096
the final code:
.directive('ckEditor', function() {
return {
require : '?ngModel',
link : function($scope, elm, attr, ngModel) {
var ck = CKEDITOR.replace(elm[0]);
ck.on('instanceReady', function() {
ck.setData(ngModel.$viewValue);
});
ck.on('pasteState', function() {
$scope.$apply(function() {
ngModel.$setViewValue(ck.getData());
});
});
ngModel.$render = function(value) {
ck.setData(ngModel.$modelValue);
};
}
};
})
edit: removed unused brackets

Thanks to Vojta for the excellent directive. Sometimes it doesn't load. Here is a modified version to fix that issue.
angular.module('ck', []).directive('ckEditor', function() {
var calledEarly, loaded;
loaded = false;
calledEarly = false;
return {
require: '?ngModel',
compile: function(element, attributes, transclude) {
var loadIt, local;
local = this;
loadIt = function() {
return calledEarly = true;
};
element.ready(function() {
return loadIt();
});
return {
post: function($scope, element, attributes, controller) {
if (calledEarly) {
return local.link($scope, element, attributes, controller);
}
loadIt = (function($scope, element, attributes, controller) {
return function() {
local.link($scope, element, attributes, controller);
};
})($scope, element, attributes, controller);
}
};
},
link: function($scope, elm, attr, ngModel) {
var ck;
if (!ngModel) {
return;
}
if (calledEarly && !loaded) {
return loaded = true;
}
loaded = false;
ck = CKEDITOR.replace(elm[0]);
ck.on('pasteState', function() {
$scope.$apply(function() {
ngModel.$setViewValue(ck.getData());
});
});
ngModel.$render = function(value) {
ck.setData(ngModel.$viewValue);
};
}
};
});
or if you need it in coffeescript
angular.module('ck', []).directive('ckEditor', ->
loaded = false
calledEarly = false
{
require: '?ngModel',
compile: (element, attributes, transclude) ->
local = #
loadIt = ->
calledEarly = true
element.ready ->
loadIt()
post: ($scope, element, attributes, controller) ->
return local.link $scope, element, attributes, controller if calledEarly
loadIt = (($scope, element, attributes, controller) ->
return ->
local.link $scope, element, attributes, controller
)($scope, element, attributes, controller)
link: ($scope, elm, attr, ngModel) ->
return unless ngModel
if (calledEarly and not loaded)
return loaded = true
loaded = false
ck = CKEDITOR.replace(elm[0])
ck.on('pasteState', ->
$scope.$apply( ->
ngModel.$setViewValue(ck.getData())
)
)
ngModel.$render = (value) ->
ck.setData(ngModel.$viewValue)
}
)

And just for the record if you want to use multiple editors in one page this can come in handy:
mainApp.directive('ckEditor', function() {
return {
restrict: 'A', // only activate on element attribute
scope: false,
require: 'ngModel',
controller: function($scope, $element, $attrs) {}, //open for now
link: function($scope, element, attr, ngModel, ngModelCtrl) {
if(!ngModel) return; // do nothing if no ng-model you might want to remove this
element.bind('click', function(){
for(var name in CKEDITOR.instances)
CKEDITOR.instances[name].destroy();
var ck = CKEDITOR.replace(element[0]);
ck.on('instanceReady', function() {
ck.setData(ngModel.$viewValue);
});
ck.on('pasteState', function() {
$scope.$apply(function() {
ngModel.$setViewValue(ck.getData());
});
});
ngModel.$render = function(value) {
ck.setData(ngModel.$viewValue);
};
});
}
}
});
This will destroy all previous instances of ckeditor and create a new one.

There's now a 'change' event that can be used for this.
Here's a directive I've just created that has a couple of different toolbar config options, I'm using the jQuery adapter to initialize ckeditor. For more info check out this blog post.
(function () {
'use strict';
angular
.module('app')
.directive('wysiwyg', Directive);
function Directive($rootScope) {
return {
require: 'ngModel',
link: function (scope, element, attr, ngModel) {
var editorOptions;
if (attr.wysiwyg === 'minimal') {
// minimal editor
editorOptions = {
height: 100,
toolbar: [
{ name: 'basic', items: ['Bold', 'Italic', 'Underline'] },
{ name: 'links', items: ['Link', 'Unlink'] },
{ name: 'tools', items: ['Maximize'] },
{ name: 'document', items: ['Source'] },
],
removePlugins: 'elementspath',
resize_enabled: false
};
} else {
// regular editor
editorOptions = {
filebrowserImageUploadUrl: $rootScope.globals.apiUrl + '/files/uploadCk',
removeButtons: 'About,Form,Checkbox,Radio,TextField,Textarea,Select,Button,ImageButton,HiddenField,Save,CreateDiv,Language,BidiLtr,BidiRtl,Flash,Iframe,addFile,Styles',
extraPlugins: 'simpleuploads,imagesfromword'
};
}
// enable ckeditor
var ckeditor = element.ckeditor(editorOptions);
// update ngModel on change
ckeditor.editor.on('change', function () {
ngModel.$setViewValue(this.getData());
});
}
};
}
})();
Here's a couple of examples of how to use the directive in HTML
<textarea ng-model="vm.article.Body" wysiwyg></textarea>
<textarea ng-model="vm.article.Body" wysiwyg="minimal"></textarea>
And here are the CKEditor scripts I'm including from a CDN plus a couple of extra plugins that I downloaded to enable pasting images from word.
<script src="//cdn.ckeditor.com/4.5.7/full/ckeditor.js"></script>
<script src="//cdn.ckeditor.com/4.5.7/full/adapters/jquery.js"></script>
<script type="text/javascript">
// load extra ckeditor plugins
CKEDITOR.plugins.addExternal('simpleuploads', '/js/ckeditor/plugins/simpleuploads/plugin.js');
CKEDITOR.plugins.addExternal('imagesfromword', '/js/ckeditor/plugins/imagesfromword/plugin.js');
</script>

ES6 example with CKEditor v5.
Register directive using:
angular.module('ckeditor', []).directive('ckEditor', CkEditorDirective.create)
Directive:
import CkEditor from "#ckeditor/ckeditor5-build-classic";
export default class CkEditorDirective {
constructor() {
this.restrict = 'A';
this.require = 'ngModel';
}
static create() {
return new CkEditorDirective();
}
link(scope, elem, attr, ngModel) {
CkEditor.create(elem[0]).then((editor) => {
editor.document.on('changesDone', () => {
scope.$apply(() => {
ngModel.$setViewValue(editor.getData());
});
});
ngModel.$render = () => {
editor.setData(ngModel.$modelValue);
};
scope.$on('$destroy', () => {
editor.destroy();
});
})
}
}

Related

how to make component not re-render when next page

i'm doing asynchronous processing while waiting for created to finish then start running mouted , everything is fine, but something is causing my component to re-render, looks like this: video
how do i handle the above problem
here is my code:
<template>
<div class="wrapper">
<div class="main-panel">
<dashboard-content #click.native="toggleSidebar" />
</div>
<Sidebar :sidebar-data="dataSidebar"/>
</div>
</template>
data() {
return {
dataSidebar: [],
role: adminRole.OWNER,
isPending: null, // Save promise handler
};
},
created() {
if (!(STORE_ADMIN_AUTH_KEY in this.$store._modules.root._children)) {
this.$store.registerModule(STORE_ADMIN_AUTH_KEY, store);
}
if (localStorage.getItem(ADMIN_AUTH_TOKEN_KEY)) {
const res = this.$store.dispatch(STORE_ADMIN_AUTH_KEY + "/getInfo");
this.isPending = new Promise((solver, reject) => {
res.then((data) => {
localStorage.setItem("AUTH",JSON.stringify(data.role ? data.role : adminRole.OWNER));
solver();
});
});
}
},
async mounted() {
await this.isPending;
this.getSitebarItems();
},
methods: {
getSitebarItems() {
if (localStorage.getItem("AUTH")) {
this.role = localStorage.getItem("AUTH");
}
if (this.role == adminRole.OWNER) {
this.dataSidebar = sidebarItems;
return;
}
sidebarItems.forEach((element) => {
if (element.onlyOwner == 0) {
this.dataSidebar.push(element);
}
});
},
},
thanks for your help!
Maybe you could try creating a copy of the items to prevent triggering reactivity.
getSitebarItems() {
let data = sidebarItems.slice();
if (this.role == adminRole.OWNER) {
this.dataSidebar = data;
return;
}
data = data.filter((element) => {
return element.onlyOwner == 0;
});
this.dataSidebar = data;
}

Nuxt - Cannot access data inside a method

I'm using CKEditor 5 + CKFinder (Modal Mode) to select an image using the #click event. The problem is that I don't have access to data inside the onInit function.
Here is the method:
data() {
return {
post: {
thumbnail: null,
},
};
},
methods: {
openModal() {
console.log(this.post.thumbnail) // NO PROBLEM! this.post.thumbnail IS ACCESSIBLE
CKFinder.modal( {
chooseFiles: true,
width: 800,
height: 600,
onInit: function( finder ) {
finder.on( 'files:choose', function( evt ) {
var file = evt.data.files.first();
this.post.thumbnail = file.getUrl(); // PROBLEM !! $this.post is undefined
} );
}
} );
},
},
And this is my Template:
<div class="btn btn-danger" #click="openModal">Choose Image</div>
<img class="mx-auto d-block" :src="post.thumbnail" />
As mentioned in the comment, you need to use an arrow function to resolve this in the vue object.
Whenever you use function () {}, this refers to the properties of the function, not the Vue object that you intend to reference
// example
methods () {
openModal() {
onInit: function () {
this.post // 'this' is the onInit function, not the Vue object
}
}
}
// solution
methods () {
openModal() {
onInit: () => {
this.post // 'this' is the Vue object
}
}
}
Answer
data() {
return {
post: {
thumbnail: null,
},
};
},
methods: {
openModal() {
console.log(this.post.thumbnail) // NO PROBLEM! this.post.thumbnail IS ACCESSIBLE
CKFinder.modal( {
chooseFiles: true,
width: 800,
height: 600,
onInit: finder => {
finder.on( 'files:choose', evt => {
var file = evt.data.files.first();
this.post.thumbnail = file.getUrl(); // PROBLEM !! $this.post is undefined
});
}
});
},
},

Can't get jsonp callback to work inside a Vue component

I'm trying to take this Flickr jsonp Vue example (https://codepen.io/tomascherry/pen/GrgbzQ) and turn it into a component. However, I cannot figure out how to get jsonFlickrFeed() to map to this.jsonFlickrFeed() (once that function is place inside the component's methods: {}).
Code as follows:
<template>
<!-- HTML HERE -->
</template>
<script>
let callApiTimeout = null
export default {
name: 'Flickr',
filters: {
splitTags: function(value) {
// showing only first 5 tags
return value.split(' ').slice(0, 5)
}
},
directives: {
/* VueJs utilites */
img: {
inserted: function(el, binding) {
this.lazyload(el, binding)
},
update: function(el, binding) {
this.lazyload(el, binding)
}
}
},
data() {
return {
images: [],
query: ''
}
},
watch: {
query: function(value) {
clearTimeout(callApiTimeout)
callApiTimeout = setTimeout(
function() {
const reqURL =
'https://api.flickr.com/services/feeds/photos_public.gne'
const options = {
params: {
format: 'json',
tags: this.query,
jsoncallback: 'this.jsonFlickrFeed'
}
}
this.$http.jsonp(reqURL, options)
}.bind(this),
250
)
}
},
methods: {
/* JSONP callback function */
jsonFlickrFeed(response) {
this.$data.images = response.items
},
/* General utility functions */
lazyload(el, binding) {
const img = new Image()
img.src = binding.value
img.onload = function() {
el.src = binding.value
}
}
}
}
</script>
<style lang="less">
/* STYLE HERE */
</style>
I tried adding the jsoncallback: 'this.jsonFlickrFeed' parameter but that doesn't help.
To make it simpler, just pass the parameter nojsoncallback=1 and it will return the JSON object directly.

How to make sure my directive runs a function?

I have this Vue code:
export default {
data: function() {
return {
'showPopup': false
}
},
components: {
'search-bar': SearchBarComponent,
},
mounted: function() {
$(this.$el).foundation();
},
updated: function() {
$(this.$el).foundation();
},
methods: {
clickOutsidePopup: function(event) {
console.log(event);
}
},
directives: {
clickoutside: {
bind (el) {
el.event = event => el.vm.$emit(el.expression, event)
el.addEventListener('click', el.stopProp)
document.body.addEventListener('click', event)
},
unbind(el) {
el.removeEventListener('click', el.stopProp)
document.body.removeEventListener('click', el.event)
},
stopProp(event) { event.stopPropagation() }
}
}
}
And inside the template I have this:
<div class="small-screen popup-container">
<div class="popup" v-show="showPopup" v-clickoutside="clickOutsidePopup">
<search-bar />
</div>
</div>
which will be shown/hidden if we click on this:
<span #click="showPopup = !showPopup">🔍</span>
My problem is that my directive does not execute clickOutsidePopup. When I click outside of my element? I was inspired by this: Detect click outside element
I managed to make it work with this directive code:
directives: {
clickoutside: {
bind: function (el, binding, vnode) {
el.clickOutsideEvent = function (event) {
// here I check that click was outside the el and his childrens
if (!(el == event.target || el.contains(event.target))) {
// and if it did, call method provided in attribute value
vnode.context[binding.expression](event);
}
};
document.body.addEventListener('click', el.clickOutsideEvent)
},
unbind: function (el) {
document.body.removeEventListener('click', el.clickOutsideEvent)
},
}
}
added an id to the search button:
<span id="top-bar-search-icon" #click="showPopup = !sho wPopup">🔍</span>
and modified my method:
methods: {
clickOutsidePopup: function(event) {
if (event.target.id !== "top-bar-search-icon")
this.showPopup = false;
}
},

Dynamic html elements in Vue.js

How is it possible to add elements dynamically to the content? Example below:
<template>
{{{ message | hashTags }}}
</template>
<script>
export default {
...
filters: {
hashTags: function(value) {
// Replace hash tags with links
return value.replace(/#(\S*)/g, '<a v-on:click="someAction()">#$1</a>')
}
}
}
</script>
Problem is that if I press the link no action will fire. Vue do not see new elements.
Update:
Based on this answer, you can do a similar dynamic-template component in Vue 2. You can actually set up the component spec in the computed section and bind it using :is
var v = new Vue({
el: '#vue',
data: {
message: 'hi #linky'
},
computed: {
dynamicComponent: function() {
return {
template: `<div>${this.hashTags(this.message)}</div>`,
methods: {
someAction() {
console.log("Action!");
}
}
}
}
},
methods: {
hashTags: function(value) {
// Replace hash tags with links
return value.replace(/#(\S*)/g, '<a v-on:click="someAction">#$1</a>')
}
}
});
setTimeout(() => {
v.message = 'another #thing';
}, 2000);
<script src="//unpkg.com/vue#latest/dist/vue.js"></script>
<div id="vue">
<component :is="dynamicComponent" />
</div>
Vue bindings don't happen on interpolated HTML. You need something Vue sees as a template, like a partial. However, Vue only applies bindings to a partial once; you can't go back and change the template text and have it re-bind. So each time the template text changes, you have to create a new partial.
There is a <partial> tag/element you can put in your HTML, and it accepts a variable name, so the procedure is:
the template HTML changes
register new partial name for the new template HTML
update name variable so the new partial is rendered
It's a little bit horrible to register something new every time there's a change, so it would be preferable to use a component with a more structured template if possible, but if you really need completely dynamic HTML with bindings, it works.
The example below starts out with one message, link-ified as per your filter, and after two seconds, changes message.
You can just use message as the name of the partial for registering, but you need a computed that returns that name after doing the registering, otherwise it would try to render before the name was registered.
var v = new Vue({
el: 'body',
data: {
message: 'hi #linky'
},
computed: {
partialName: function() {
Vue.partial(this.message, this.hashTags(this.message));
return this.message;
}
},
methods: {
someAction: function() {
console.log('Action!');
},
hashTags: function(value) {
// Replace hash tags with links
return value.replace(/#(\S*)/g, '<a v-on:click="someAction()">#$1</a>')
}
}
});
setTimeout(() => {
v.$set('message', 'another #thing');
}, 2000);
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/1.0.26/vue.min.js"></script>
<partial :name="partialName"></partial>
I just learned about $compile, and it seems to fit your need very nicely. A very simple directive using $compile avoids all the registrations.
Vue.directive('dynamic', function(newValue) {
this.el.innerHTML = newValue;
this.vm.$compile(this.el);
});
var v = new Vue({
el: 'body',
data: {
message: 'hi #linky'
},
computed: {
messageAsHtml: function() {
return this.message.replace(/#(\S*)/g, '<a v-on:click="someAction()">#$1</a>');
}
},
methods: {
someAction: function() {
console.log('Action!');
}
}
});
setTimeout(() => {
v.$set('message', 'another #thing');
}, 2000);
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/1.0.26/vue.min.js"></script>
<div v-dynamic="messageAsHtml"></div>
In Vue.js 2 it's easier:
new Vue({
...,
computed: {
inner_html() {
return ...; // any raw html
},
},
template: `<div v-html='inner_html'></div>`,
});
The best solution I found which works fine with custom html is looks like this, it's like you kind of create new component each times the html property changes. No actually one did this, we just use computed property for creating new component.
That is how it looks:
new Vue({
el: "#root",
data: {
value: '',
name: 'root',
htmlData: '<div><input #input="onInputProxy($event)" ' +
'v-model="value" ' +
'v-for="i in 3" ' +
':ref="`customInput${i}`"></div>'
},
computed: {
// our component is computed property which returns the dict
htmlDataComponent () {
return {
template: this.htmlData, // we use htmlData as template text
data() {
return {
name: 'component',
value: ''
}
},
created () {
// value of "this" is formComponent
console.log(this.name + ' created');
},
methods: {
// proxy components method to parent method,
// actually you done have to
onInputProxy: this.onInput
}
}
}
},
methods: {
onInput ($event) {
// while $event is proxied from dynamic formComponent
// value of "this" is parent component
console.log(this.name + ' onInput');
// use refs to refer to real components value
console.log(this.$refs.htmlDataComponent.value);
console.log(this.$refs.htmlDataComponent.$refs.customInput1);
console.log(this.$refs.htmlDataComponent.$refs.customInput2);
console.log(this.$refs.htmlDataComponent.$refs.customInput3);
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.1.10/vue.min.js">
</script>
<div id="root">
<component ref="htmlDataComponent"
v-if="htmlData"
:is="htmlDataComponent"></component>
</div>
I did not check it for memory efficiency, but it looks like works just fine.
Modified version of #RoyJ's answer, works in Vue.js v2.6.10
new Vue({
...,
computed: {
inner_html() {
return ...; // any raw html
},
},
directives: {
dynamic: {
bind(el, binding) {
el.innerHTML = binding.value;
},
update(el, binding) {
el.innerHTML = binding.value;
},
},
},
template: `<div v-dynamic='inner_html'></div>`,
});
Since partial has been removed from VueJS 2 (https://v2.vuejs.org/v2/guide/migration.html#Vue-partial-removed)
A better way may be to create a component which processes its content and create appropriate DOM elements
The above component will replace hashtags by clickable links
<process-text>Hi #hashtag !</process-text>
Vue.component('process-text', {
render: function (createElement) {
var hashtagRegex = /(^|\W)(#[a-z\d][\w-]*)/ig
var text = this.$slots.default[0].text
var list = text.split(hashtagRegex)
var children = []
for (var i = 0; i < list.length; i++) {
var element = list[i]
if (element.match(hashtagRegex)) {
children.push(createElement('a', {
attrs: {
href: 'https://www.google.fr/search?q=' + element,
target: "_blank"
},
domProps: {
innerHTML: element
}
}))
} else {
children.push(element)
}
}
}
return createElement('p', {}, children) // VueJS expects root element
})