I'm developing a new version of my website in Vue 2 and I have created a component to handle image broken links, swapping the url domain, in order to display images stored in my local host and also those stored in the production server. The problem is that sometimes the image filename in the server is different than what is set on my local host database (because the user changed the image on the production live version), and then I get infinite requests looping in the console since neither image url are valid.
I'm also using Bootstrap-Vue to lazy-load the images.
This is my code:
<template>
<b-img-lazy :src="'https://example.com/uploads/' + photo" #error.native="replaceSrc" :alt="titulo" :title="title" class="thumb"></b-img-lazy>
</template>
<script>
export default {
name: "ImgCarousel",
props: {
photo: {
type: String
},
title: {
type: String
}
},
data: function () {
return {
index: null
}
},
methods: {
replaceSrc(e) {
e.target.src = 'http://localhost:8888/uploads/' + this.photo;
},
}
}
</script>
How do I stop the GET requests from entering a infinite loop?
I also reached the solution below. Probably not elegant as Nikola's answer, although it needs less coding. Basically it adds a counter on each call for "replaceSrc" to prevent looping on the broken links:
<template>
<b-img-lazy :src="'https://example.com/uploads/' + photo" #error.native="replaceSrc" :alt="title" :title="title" class="thumb"></b-img-lazy>
</template>
<script>
export default {
name: "ImgCarousel",
props: {
photo: {
type: String
},
title: {
type: String
}
},
data: function () {
return {
counter: 0
}
},
methods: {
replaceSrc(e) {
if (this.counter == 0) {
e.target.src = 'http://localhost:8888/uploads/' + this.photo;
this.counter ++;
} else if (this.counter == 1) {
e.target.src = 'https://via.placeholder.com/300x190';
this.counter ++;
}
},
}
}
</script>
BTW the "index: null" data on my question was a wrong code I forgot to remove.
Try to check if image exists:
new Vue({
el: '#demo',
data: function () {
return {
startImg: 'https://picsum.photos/150',
index: null,
photo: '100',
title: '',
titulo: '',
url: 'https://picsum.photos/'
}
},
methods: {
replaceSrc(e) {
this.checkIfImageExists(this.url + this.photo, (exists) => {
exists ?
e.target.src = this.url + this.photo :
this.titulo = 'no image'
})
},
checkIfImageExists(url, callback) {
const img = new Image();
img.src = url;
if (img.complete) {
callback(true);
} else {
img.onload = () => callback(true)
img.onerror = () => callback(false)
}
},
change() {
this.startImg = this.startImg ? '' : 'https://picsum.photos/150'
},
breakSource() {
this.startImg = "wrong url"
this.photo = "wrong url"
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<link type="text/css" rel="stylesheet" href="https://unpkg.com/bootstrap/dist/css/bootstrap.min.css" />
<link type="text/css" rel="stylesheet" href="https://unpkg.com/bootstrap-vue#latest/dist/bootstrap-vue.min.css" />
<script src="https://unpkg.com/bootstrap-vue#latest/dist/bootstrap-vue.min.js"></script>
<script src="https://unpkg.com/bootstrap-vue#latest/dist/bootstrap-vue-icons.min.js"></script>
<div id="demo">
<div>
<b-img-lazy :src="startImg" #error.native="replaceSrc" :title="title" :alt="titulo" class="thumb"></b-img-lazy>
</div>
<button #click="change">Change source</button>
<button #click="breakSource">Break source</button>
</div>
I am evaluating the web-test-runner with playwright from modern-web (https://modern-web.dev/docs/test-runner/overview/) to test my web-components.
My sample project is the following:
./package.json
{
"scripts": {
"test": "web-test-runner \"**/*.test.html\" \"**/*.test.js\" --node-resolve --playwright --browsers chromium firefox webkit --coverage",
"test:watch": "web-test-runner \"**/*.test.html\" \"**/*.test.js\" --node-resolve --playwright --browsers chromium firefox webkit --watch"
},
"devDependencies": {
"#esm-bundle/chai": "^4.1.5",
"#web/test-runner": "^0.10.0",
"#web/test-runner-playwright": "^0.6.6"
}
}
./my-component/my-component.js
(async() => {
const res = await fetch('/my-component/my-component.html');
const template = document.createElement('template');
template.innerHTML = await res.text();
class MyComponent extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.appendChild(template.content.cloneNode(true));
}
}
window.customElements.define('my-component', MyComponent);
})();
./my-component/my-component.html
<style>
h2 {
color: red;
}
</style>
<h2>Hello world!</h2>
./my-component/test/my-component.test.html
<html>
<head>
<script src="/my-component/my-component.js"></script>
</head>
<body>
<script type="module">
import { runTests } from '#web/test-runner-mocha';
import { expect } from '#esm-bundle/chai';
let element;
runTests(async () => {
describe('HTML tests', () => {
beforeEach(() => {
element = document.createElement("my-component");
document.body.appendChild(element);
});
afterEach(() => {
document.body.removeChild(element);
});
it('component is loaded', () => {
expect(element.shadowRoot).to.exist;
});
it('component contains h2 with text', () => {
expect(element.shadowRoot.querySelector('h2').innerHTML).to.equal('Hello world!');
});
it('component is displayed in red color', () => {
const el = element.shadowRoot.querySelector('h2');
expect(getComputedStyle(el)).to.have.property('color', 'rgb(255, 0, 0)');
});
});
});
</script>
</body>
</html>
this test is passing successfully.
according to modern-web documentation, it is also possible to create tests within js files. So, I am trying with this test:
./my-component/test/my-component.test.js
import { runTests } from '#web/test-runner-mocha';
import { expect } from '#esm-bundle/chai';
let element;
runTests(async () => {
describe('HTML tests', () => {
before(() => {
const script = document.createElement("script");
script.setAttribute("src", "/my-component/my-component.js");
document.head.appendChild(script);
});
beforeEach(() => {
element = document.createElement("my-component");
document.body.appendChild(element);
});
afterEach(() => {
document.body.removeChild(element);
});
it('component is loaded', () => {
expect(element.shadowRoot).to.exist;
});
it('component contains h2 with text', () => {
expect(element.shadowRoot.querySelector('h2').innerHTML).to.equal('Hello world!');
});
it('component is displayed in red color', () => {
const el = element.shadowRoot.querySelector('h2');
expect(getComputedStyle(el)).to.have.property('color', 'rgb(255, 0, 0)');
});
});
});
This is basically the same as for the html test with the addition of the before directive to register my component script.
When running the js test, I get this error:
my-component/test/my-component.test.js:
❌ Tests were interrupted because the page was reloaded. This can happen when clicking a link, submitting a form or interacting with window.location.
Chromium: |██████████████████████████████| 1/1 test files | 0 passed, 0 failed
Firefox: |██████████████████████████████| 1/1 test files | 0 passed, 0 failed
Webkit: |██████████████████████████████| 1/1 test files | 0 passed, 0 failed
View full coverage report at coverage/lcov-report/index.html
How could I solve it?
Best Regards,
In our application, there is large set of data (json ~ 630KB) getting populated which consumes around 219.28 MB of JavaScript memory displayed in chrome task manager.
It loads the data from the api and uses vuex to store the data and generates the menu recursively.
Since it is a big application, I mimicked the same behaviour with a sample vue.js application by loading 100000 records to understand how the memory utilization works.
Each time "Run test" is clicked, 200 MB memory is consumed in the task manager and is not released until so much time even if the page is refreshed.
I tried to use "beforeDestroy" hook to clear the object from vuex but it is not working. I would like to know if the data itself is causing the slow performance or any other improvements can be done.
<html>
<head>
</head>
<body>
<div id="myApp">
<button #click="setCounterModule()">Counter</button>
<button #click="setCustomersModule">Customers</button>
<br />
<br />
<div>
<button-counter v-if="currentModule=='counter'"></button-counter>
<customers v-else></customers>
</div>
</div>
<script src="vue.js"></script>
<script src="vuex.js"></script>
<script>
Vue.component('button-counter', {
data: function () {
return {
count: 0
}
},
template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>'
});
Vue.component('customers', {
computed: {
customers() {
return store.state.customers
}
},
template: '<div><button #click="testMe" :disabled="loading">Run Test</button><div v-for="c in customers">{{ c.name }}</div></div>',
data: function() {
return {
loading: false
}
},
methods: {
testMe: function () {
this.loading = true;
let customers = [];
for (var i=0; i < 100000; i++) {
let customer = {'name': 'John' +i, 'address': 'Addr' + i};
customers.push(customer);
}
store.commit('assignCustomers', customers);
this.loading = false;
}
},
beforeDestroy() {
// console.log('vuex destroyed');
// store.commit('assignCustomers', null);
}
});
const store = new Vuex.Store({
state: {
customers: null
},
mutations: {
assignCustomers (state, customers) {
state.customers = customers;
}
}
});
var app = new Vue( {
el: "#myApp",
data: function() {
return {
currentModule: "counter"
}
},
methods: {
setCounterModule: function() {
this.currentModule = "counter";
},
setCustomersModule: function() {
this.currentModule = "customers";
}
}
});
</script>
</body>
</html>
I'm trying to POST a token and display the results as an ordered list. I would like the list to update onchange of the input. On page load the request POST is infinitely looping with error:
TypeError: ctrl.keys(...) is undefined
I suspect that my assumptions of how the data binding on the controller works are completely wrong.
//component
var tISM = {};
//model
tISM = {
Key: function(data) {
this.Id = m.prop(data.Id);
this.Name = m.prop(data.Name);
this.CreationTime = m.prop(data.CreationTime);
},
keys: function(token) {
return m.request({
method: "POST",
url: "key/list",
data: { "token": token },
type: tISM.Key
})
},
};
//controller
tISM.controller = function() {
// when this controller is updated perform a new POST for data by calling message?
var token = m.prop("xxx")
var keys = function() {
tISM.keys(this.token)
}
return { token, keys }
};
//view
tISM.view = function(ctrl) {
return m("div"), [
m("ol", ctrl.keys().map( function(key, index) {
return m("li", key.Id, key.Name, key.CreationTime)
})),
m("input", {
onchange: m.withAttr("value", ctrl.token)
})
]
};
//initialize
m.mount(document.getElementById("app"), tISM);
<script src="http://cdnjs.cloudflare.com/ajax/libs/mithril/0.2.5/mithril.min.js"></script>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Mithril App</title>
<script src="mithril.js"></script>
</head>
<body>
<div id="app"></div>
</body>
</html>
m.request() returns a deferred, not the value itself. I have a snippet below that shows one way of getting the values out. I explicitly replaced the m.request() with deferred calls under the hood, and a timeout rather than a post.
//component
var tISM = {};
//model
tISM = {
Key: function(data) {
this.Id = m.prop(data.Id);
this.Name = m.prop(data.Name);
this.CreationTime = m.prop(data.CreationTime);
},
keys: function(token) {
m.startComputation()
var d = m.deferred();
setTimeout(function() {
d.resolve([1,2,3]);
m.endComputation();
}, 1000);
return d.promise;
}
};
//controller
tISM.controller = function() {
// when this controller is updated perform a new POST for data by calling message?
var token = m.prop("xxx")
var keys = m.prop([]);
tISM.keys(this.token).then(keys)
return {
token: token,
keys: keys
}
return { token, keys }
};
//view
tISM.view = function(ctrl) {
return m("div"), [
m("ol", ctrl.keys().map( function(key, index) {
return m("li", key.Id, key.Name, key.CreationTime)
})),
m("input", {
onchange: m.withAttr("value", ctrl.token)
})
]
};
//initialize
m.mount(document.getElementById("app"), tISM);
<script src="http://cdnjs.cloudflare.com/ajax/libs/mithril/0.2.5/mithril.min.js"></script>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Mithril App</title>
<script src="mithril.js"></script>
</head>
<body>
<div id="app"></div>
</body>
</html>
My problem was I don't know javascript. This works:
//component
var tISM = {};
//model
tISM.Key = function(data) {
this.Id = m.prop(data.Id);
this.Name = m.prop(data.Name);
this.CreationTime = m.prop(data.CreationTime);
}
tISM = {
keys: function(token) {
return m.request({
method: "POST",
url: "key/list",
data: { "token": token },
type: tISM.Key
})
},
};
//controller
tISM.controller = function() {
this.token = m.prop("")
this.keys = m.prop([])
this.updateToken = function(token) {
this.token(token)
tISM.keys(this.token).then(this.keys)
}.bind(this)
};
//view
tISM.view = function(ctrl) {
return m("div"), [
m("input", {
oninput: m.withAttr("value", ctrl.updateToken)
}),
m("ol", ctrl.keys().map( function(key, index) {
return m("li", key.Id, key.Name, key.CreationTime)
})),
]
};
//initialize
m.mount(document.getElementById("app"), tISM);
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();
});
})
}
}