Piranha CMS - Custom block won't save - piranha-cms

Very new to Piranha and Vue, but not to .Net Core. Trying to get my arms around how to create custom blocks. I've created a new block, attempting to marry the HtmlBlock and ImageBlock:
using Piranha.Extend;
using Piranha.Extend.Blocks;
using Piranha.Extend.Fields;
namespace YR.Models.Piranha.Blocks
{
[BlockType(Name = "Card", Category = "Content", Icon = "fas fa-address-card", Component = "card-block")]
public class CardBlock : Block
{
public ImageField ImgBody { get; set; }
public SelectField<ImageAspect> Aspect { get; set; } = new SelectField<ImageAspect>();
public HtmlField HtmlBody { get; set; }
public override string GetTitle()
{
if (ImgBody != null && ImgBody.Media != null)
{
return ImgBody.Media.Filename;
}
return "No image selected";
}
}
}
If I omit the Component property in the BlockTypeAttribute, the block works, saves the image and content to draft and updates the site perfectly when published. For the full experience in the manager, I tried to also build a Vue component that just combined both the html-block.vue and image-block.vue components.
Here's what I have for the Vue component:
Vue.component("card-block", {
props: ["uid", "toolbar", "model"],
data: function () {
return {
imgBody: this.model.imgBody.value,
htmlBody: this.model.htmlBody.value
};
},
methods: {
clear: function () {
// clear media from block
},
onBlur: function (e) {
this.model.htmlBody.value = e.target.innerHTML;
},
onChange: function (data) {
this.model.htmlBody.value = data;
},
remove: function () {
this.model.imgBody.id = null;
this.model.imgBody.media = null;
},
select: function () {
if (this.model.imgBody.media != null) {
piranha.mediapicker.open(this.update, "Image", this.model.imgBody.media.folderId);
} else {
piranha.mediapicker.openCurrentFolder(this.update, "Image");
}
},
update: function (media) {
if (media.type === "Image") {
this.model.imgBody.id = media.id;
this.model.imgBody.media = media;
// Tell parent that title has been updated
this.$emit('update-title', {
uid: this.uid,
title: this.model.imgBody.media.filename
});
} else {
console.log("No image was selected");
}
}
},
computed: {
isEmpty: function () {
return {
htmlBody: piranha.utils.isEmptyHtml(this.model.htmlBody.value),
imgBody: this.model.imgBody.media == null
}
},
mediaUrl: function () {
if (this.model.imgBody.media != null) {
return piranha.utils.formatUrl(this.model.imgBody.media.publicUrl);
} else {
return piranha.utils.formatUrl("~/manager/assets/img/empty-image.png");
}
}
},
mounted: function () {
piranha.editor.addInline(this.uid, this.toolbar, this.onChange);
this.model.imgBody.getTitle = function () {
if (this.model.imgBody.media != null) {
return this.model.imgBody.media.filename;
} else {
return "No image selected";
}
};
},
beforeDestroy: function () {
piranha.editor.remove(this.uid);
},
template:
"<div class='block-body has-media-picker rounded' :class='{ empty: isEmpty }'>" +
" <img class='rounded' :src='mediaUrl'>" +
" <div class='media-picker'>" +
" <div class='btn-group float-right'>" +
" <button :id='uid' class='btn btn-info btn-aspect text-center' data-toggle='dropdown' aria-haspopup='true' aria-expanded='false'>" +
" <i v-if='model.aspect.value === 0' class='fas fa-cog'></i>" +
" <img v-else :src='iconUrl'>" +
" </button>" +
" <div class='dropdown-menu aspect-menu' :aria-labelledby='uid'>" +
" <label class='mb-0'>{{ piranha.resources.texts.aspectLabel }}</label>" +
" <div class='dropdown-divider'></div>" +
" <a v-on:click.prevent='selectAspect(0)' class='dropdown-item' :class='{ active: isAspectSelected(0) }' href='#'>" +
" <img :src='piranha.utils.formatUrl('~/manager/assets/img/icons/img-original.svg')'><span>{{ piranha.resources.texts.aspectOriginal }}</span>" +
" </a>" +
" <a v-on:click.prevent='selectAspect(1)' class='dropdown-item' :class='{ active: isAspectSelected(1) }' href='#'>" +
" <img :src='piranha.utils.formatUrl('~/manager/assets/img/icons/img-original.svg')'><span>{{ piranha.resources.texts.aspectOriginal }}</span>" +
" </a>" +
" <a v-on:click.prevent='selectAspect(2)' class='dropdown-item' :class='{ active: isAspectSelected(2) }' href='#'>" +
" <img :src='piranha.utils.formatUrl('~/manager/assets/img/icons/img-original.svg')'><span>{{ piranha.resources.texts.aspectOriginal }}</span>" +
" </a>" +
" <a v-on:click.prevent='selectAspect(3)' class='dropdown-item' :class='{ active: isAspectSelected(3) }' href='#'>" +
" <img :src='piranha.utils.formatUrl('~/manager/assets/img/icons/img-original.svg')'><span>{{ piranha.resources.texts.aspectOriginal }}</span>" +
" </a>" +
" <a v-on:click.prevent='selectAspect(4)' class='dropdown-item' :class='{ active: isAspectSelected(4) }' href='#'>" +
" <img :src='piranha.utils.formatUrl('~/manager/assets/img/icons/img-original.svg')'><span>{{ piranha.resources.texts.aspectOriginal }}</span>" +
" </a>" +
" </div>" +
" <button v-on:click.prevent='select' class='btn btn-primary text-center'>" +
" <i class='fas fa-plus'></i>" +
" </button>" +
" <button v-on:click.prevent='remove' class='btn btn-danger text-center'>" +
" <i class='fas fa-times'></i>" +
" </button>" +
" </div>" +
" <div class='card text-left'>" +
" <div class='card-body' v-if='isEmpty'>" +
" " +
" </div>" +
" <div class='card-body' v-else>" +
" {{ model.body.media.filename }}" +
" </div>" +
" </div>" +
"</div>" +
" <div contenteditable='true' :id='uid' spellcheck='false' v-html='htmlBody' v-on:blur='onBlur'></div>" +
"</div>"
});
It's basically a meld of the two Vue components that I found in the repo on GitHub, but I've massaged it a little to get it to not spit out errors in the DevTools console. I'll revisit those items if I can just get past saving it.
So, here are my questions for #tidyui or anyone who's successfully implemented something like this:
Am I going about this the right way? I simply want three columns, each column would contain my CardBlock which has a picture and a blurb of content under it, but I want the CardBlock to be a single unit (kind of like a Bootstrap Card). Is there already a way to do this without creating my own block? I researched nesting BlockGroups, but quickly found out that it isn't possible.
If I'm on the right track, I need help with the error I'm getting when I attempt to save the draft. The error is identical to Save ImageBlock error #1117, which appears to have been fixed in 8.2. I'm on 8.4.2.
I absolutely LOVE the idea of a CMS built for .Net Core, and Piranha blows away the CMS I created for myself. I just need a little push in the right direction, please, I've been at this for most of the day.
Thanks in advance,
D

Just in case this helps someone. Turns out I did a poor job merging the two blocks together. Chalk this one up to inexperience with both Piranha and Vue.js. I was mixing the code from the docs with the code in the repo. Don't do that - the docs are understandably still a bit behind. I'm not throwing stones at the developers, I really dig what they've created and will continue to put forth the effort to use it proficiently.
Below is what I came up with for the Vue component. There are probably still a few tweaks to be made to separate the Image-Block and Html-Block code better, but it now works, saves, and does not throw errors in the console.
/*global
piranha
*/
Vue.component("card-block", {
props: ["uid", "toolbar", "model"],
data: function () {
return {
imgBody: this.model.imgBody.value,
htmlBody: this.model.htmlBody.value
};
},
methods: {
clear: function () {
// clear media from block
},
onBlur: function (e) {
this.model.htmlBody.value = e.target.innerHTML;
},
onChange: function (data) {
this.model.htmlBody.value = data;
},
select: function () {
if (this.model.imgBody.media != null) {
piranha.mediapicker.open(this.update, "Image", this.model.imgBody.media.folderId);
} else {
piranha.mediapicker.openCurrentFolder(this.update, "Image");
}
},
remove: function () {
this.model.imgBody.id = null;
this.model.imgBody.media = null;
},
update: function (media) {
if (media.type === "Image") {
this.model.imgBody.id = media.id;
this.model.imgBody.media = {
id: media.id,
folderId: media.folderId,
type: media.type,
filename: media.filename,
contentType: media.contentType,
publicUrl: media.publicUrl,
};
// Tell parent that title has been updated
this.$emit('update-title', {
uid: this.uid,
title: this.model.imgBody.media.filename
});
} else {
console.log("No image was selected");
}
},
selectAspect: function (val) {
this.model.aspect.value = val;
},
isAspectSelected(val) {
return this.model.aspect.value === val;
}
},
computed: {
isImgEmpty: function (e) {
return this.model.imgBody.media == null;
},
isHtmlEmpty: function () {
return piranha.utils.isEmptyHtml(this.model.htmlBody.value);
},
mediaUrl: function () {
if (this.model.imgBody.media != null) {
return piranha.utils.formatUrl(this.model.imgBody.media.publicUrl);
} else {
return piranha.utils.formatUrl("~/manager/assets/img/empty-image.png");
}
},
iconUrl: function () {
if (this.model.aspect.value > 0) {
if (this.model.aspect.value === 1 || this.model.aspect.value === 3) {
return piranha.utils.formatUrl("~/manager/assets/img/icons/img-landscape.svg");
} else if (this.model.aspect.value == 2) {
return piranha.utils.formatUrl("~/manager/assets/img/icons/img-portrait.svg");
} else if (this.model.aspect.value == 4) {
return piranha.utils.formatUrl("~/manager/assets/img/icons/img-square.svg");
}
}
return null;
}
},
mounted: function () {
piranha.editor.addInline(this.uid, this.toolbar, this.onChange);
this.model.getTitle = function () {
if (this.model.imgBody.media != null) {
return this.model.imgBody.media.filename;
} else {
return "No image selected";
}
};
},
beforeDestroy: function () {
piranha.editor.remove(this.uid);
},
template:
"<div class='block-body has-media-picker rounded' :class='{ empty: isImgEmpty }'>" +
" <div class='image-block'>" +
" <img class='rounded' :src='mediaUrl'>" +
" <div class='media-picker'>" +
" <div class='btn-group float-right'>" +
" <button :id='uid + \"-aspect\"' class='btn btn-info btn-aspect text-center' data-toggle='dropdown' aria-haspopup='true' aria-expanded='false'>" +
" <i v-if='model.aspect.value === 0' class='fas fa-cog'></i>" +
" <img v-else :src='iconUrl'>" +
" </button>" +
" <div class='dropdown-menu aspect-menu' :aria-labelledby='uid + \"-aspect\"'>" +
" <label class='mb-0'>{{ piranha.resources.texts.aspectLabel }}</label>" +
" <div class='dropdown-divider'></div>" +
" <a v-on:click.prevent='selectAspect(0)' class='dropdown-item' :class='{ active: isAspectSelected(0) }' href='#'>" +
" <img :src='piranha.utils.formatUrl(\"~/manager/assets/img/icons/img-original.svg\")'><span>{{ piranha.resources.texts.aspectOriginal }}</span>" +
" </a>" +
" <a v-on:click.prevent='selectAspect(1)' class='dropdown-item' :class='{ active: isAspectSelected(1) }' href='#'>" +
" <img :src='piranha.utils.formatUrl(\"~/manager/assets/img/icons/img-landscape.svg\")'><span>{{ piranha.resources.texts.aspectLandscape }}</span>" +
" </a>" +
" <a v-on:click.prevent='selectAspect(2)' class='dropdown-item' :class='{ active: isAspectSelected(2) }' href='#'>" +
" <img :src='piranha.utils.formatUrl(\"~/manager/assets/img/icons/img-portrait.svg\")'><span>{{ piranha.resources.texts.aspectPortrait }}</span>" +
" </a>" +
" <a v-on:click.prevent='selectAspect(3)' class='dropdown-item' :class='{ active: isAspectSelected(3) }' href='#'>" +
" <img :src='piranha.utils.formatUrl(\"~/manager/assets/img/icons/img-landscape.svg\")'><span>{{ piranha.resources.texts.aspectWidescreen }}</span>" +
" </a>" +
" <a v-on:click.prevent='selectAspect(4)' class='dropdown-item' :class='{ active: isAspectSelected(4) }' href='#'>" +
" <img :src='piranha.utils.formatUrl(\"~/manager/assets/img/icons/img-square.svg\")'><span>{{ piranha.resources.texts.aspectSquare }}</span>" +
" </a>" +
" </div>" +
" <button v-on:click.prevent='select' class='btn btn-primary text-center'>" +
" <i class='fas fa-plus'></i>" +
" </button>" +
" <button v-on:click.prevent='remove' class='btn btn-danger text-center'>" +
" <i class='fas fa-times'></i>" +
" </button>" +
" </div>" +
" <div class='card text-left'>" +
" <div class='card-body' v-if='isImgEmpty'>" +
" " +
" </div>" +
" <div class='card-body' v-else>" +
" {{ model.imgBody.media.filename }}" +
" </div>" +
" </div>" +
" </div>" +
" </div>" +
" <br />" +
" <div class='html-block'>" +
" <div class='block-body border rounded' :class='{ empty: isHtmlEmpty }' >" +
" <div contenteditable='true' :id='uid' v-html='htmlBody' v-on:blur='onBlur'></div> " +
" </div>" +
" </div>" +
"</div>"
});
I'd still love to get some confirmation that I didn't do all of this unnecessarily. If there's a better way to have done it, please don't hold back.

Related

How to render component from method of another component in Vue.js

I have a form which contains selector input, and this form is in pop-up window. Now my selector not displayed, because it renders after the main window loaded. How can I make the component which renders my selector (renderPositions), render it from method showPopup() (after the pop-up window appears)? Thanks in advance.
var usersControllerApi = Vue.resource('./users_controller/all{/id}')
var positionsControllerApi = Vue.resource('./positions_controller/all{/id}')
Vue.component('users', {
props: ['listOfUsers'],
template: '<tbody>' +
'<tr v-for="user in listOfUsers" >' +
'<td class="first_column">{{user.login}}</td>' +
'<td>{{user.fullName}}</td>' +
'<td>{{user.position.positionDescription}}</td>' +
'<td>{{user.active ? "Да" : "Нет"}} </td>' +
'<td>{{user.createDate}}</td>' +
'<td>{{user.lastUpdateDate}}</td>' +
'<td class="seven_column"><button>Изменить</button></td>' +
'<td class="eight_column"><button>Удалить</button></td>' +
'</tr>' +
'</tbody>',
created: function () {
usersControllerApi.get().then(result => result.json().then(data => data.forEach(user => {
this.listOfUsers.push(user)
}
)))
}
});
Vue.component('positions', {
props: ['listOfPositions'],
template: '<select class="form_input" id="positionsselector">' +
'<option v-for="position in listOfPositions" v-bind:value="position.positionDescription">{{position.positionDescription}}</option>' +
'</select>',
created: function () {
positionsControllerApi.get().then(result => result.json().then(data => data.forEach(pos => {
this.listOfPositions.push(pos)
}
)))
}
});
var renderUser = new Vue({
el: '#tableBody',
template: '<users :listOfUsers="listOfUsers" />',
data: {
listOfUsers: []
}
});
var renderPositions = new Vue({
el: '#positionsselector',
template: '<positions :listOfPositions="listOfPositions"/>',
data: {
listOfPositions: []
}
});
var createButtonScope = new Vue({
el: '#createButtonScope',
data: {
isPopupVisible: false,
login_input:'',
password_input:'',
repassword_input:'',
surname_input:'',
name_input:'',
},
methods: {
showPopup() {
this.isPopupVisible = true;
},
closeModalWindow() {
this.isPopupVisible = false;
},
validate() {
}
}
})
var comp = Vue.component('add-popup', {
props: {
header_title: '',
button_1_name: '',
button_2_name: ''
},
template: ' <div id="popup_window">\n' +
' <div class="Dialog">\n' +
' <div class="Dialog_overlay">\n' +
' <div class="Dialog_content">\n' +
' <div class="Dialog_header">\n' +
' <span class="modal_header_text">{{header_title}}</span>\n' +
' <span><i class="material-icons" #click="closePopup">close</i></span>\n' +
' </div>\n' +
' <div class="Dialog_body">\n' +
' <slot></slot>\n' +
' </div>\n' +
' <div class="Dialog_footer">\n' +
' <div class="modal_button_1_container">\n' +
' <button class="modal_button_1" #click="validate_form">Создать</button>\n' +
' </div>\n' +
' <div class="modal_button_2_container">\n' +
' <button class="modal_button_2" #click="closePopup">Отмена</button>\n' +
' </div>\n' +
' </div>\n' +
' </div>\n' +
' </div>\n' +
' </div>\n' +
' </div>',
methods: {
closePopup() {
this.$emit('close_popup') элементу
},
validate_form() {
this.$emit('validate')элементу
}
},
data: function () {
return {
}
}
});

How can i make an input field not update all other instances of it?

i am strting to play with Vue and i was wondering how i can update only the current edited text input value instead of updating all instances of it? I know it must be about the v-model , but i am a bit lost here.
Here is my code for the component :
Vue.component('app-list', {
template: '<section id="app-item-field" class="columns is-multiline">' +
' <div class="column is-2" v-for="(list, index) in lists" >' +
' <h2>{{list.title}}</h2>' +
' <div class="field has-addons">' +
' <input class="input is-small is-info" type="text" v-model="inputVal" placeholder="Nom de l\'item à ajouter">' +
' <a class="button is-info is-small" :disabled="initValueIsSet === false" #click="addListItem(index)">Ajouter</a>' +
' </div>' +
' <app-list-items :items="list.items"></app-list-items>' +
' </div>' +
' </section>',
props: ['lists'],
data: function () {
return {
inputVal: null
}
},
computed: {
initValueIsSet: function () {
if (this.inputVal === null) {
return false;
} else {
return this.inputVal.length > 0;
}
}
},
methods: {
addListItem: function (index) {
if (this.initValueIsSet) {
this.$emit('new-list-item', index, this.inputVal, "http://www.google.ca");
this.inputVal = "";
}
}
}
});
Thank you in advance!
The simplest solution is to turn inputVal into an array:
data: function () {
return {
inputVal: []
}
},
And use index from v-for to access it in the v-model. Template:
<input class="input is-small is-info" type="text" v-model="inputVal[index]" placeholder="Nom de l\'item à ajouter">'
Also update the methods.
Demo:
Vue.component('app-list-items', {
template: '<span></span>'
});
Vue.component('app-list', {
template: '<section id="app-item-field" class="columns is-multiline">' +
' <div class="column is-2" v-for="(list, index) in lists" >' +
' <h2>{{list.title}}</h2>' +
' <div class="field has-addons">' +
' <input class="input is-small is-info" type="text" v-model="inputVal[index]" placeholder="Nom de l\'item à ajouter"> {{ inputVal[index] }}' +
' <a class="button is-info is-small" :disabled="initValueIsSet(index) === false" #click="addListItem(index)">Ajouter</a>' +
' </div>' +
' <app-list-items :items="list.items"></app-list-items>' +
' </div>' +
' </section>',
props: ['lists'],
data: function () {
return {
inputVal: []
}
},
methods: {
initValueIsSet: function (index) {
if (this.inputVal[index] === null || this.inputVal[index] === undefined) {
return false;
} else {
return this.inputVal[index].length > 0;
}
},
addListItem: function (index) {
if (this.initValueIsSet(index)) {
this.$emit('new-list-item', index, this.inputVal[index], "http://www.google.ca");
Vue.set(this.inputVal, index, ""); // https://vuejs.org/v2/guide/reactivity.html#Change-Detection-Caveats
}
}
}
});
new Vue({
el: '#app',
data: {
lists: [{title: 'ana', items: []}, {title: 'bob', items: []}]
}
})
<script src="https://unpkg.com/vue"></script>
<div id="app">
<app-list :lists="lists"></app-list>
</div>
Since inputVal is now an array, the computed initValueIsSet is now parameterized by the index. As computeds don't take arguments, we turned it into a method.

LocalStorage in Vue App with multiple inputs

i dont know if this is possible - but i am working on a Vue app with multiple input fields that posts to the same list - and i need this to be stored somehow, so when you refresh the site, the outputs from the input fields are saved.
This means both the taskList and subTaskList array should be saved ( i know i've only worked on taskList ).
The example i posted here saves the data fine, however if you refresh, it will post all the data in all the components, can this be fixed so it will only be in the right components?
const STORAGE_KEY = 'madplan-storage'
Vue.component('list-component', {
data: function() {
return {
newTask: "",
taskList: [],
newSubTask: "",
subTaskList: [],
};
},
created() {
this.taskList = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
},
template:
'<div>' +
'<section class="prefetch">' +
'<input v-if="showInput" class="input typeahead" type="text" placeholder="Tilføj ret til madplanen" v-model="newTask" v-on:keyup.enter="addTask">' +
'</section>' +
'<details open v-for="task in taskList" v-bind:key="task.text" class="sub-list-item">' +
'<summary>{{ task.text }}<i class="fa fa-times" aria-hidden="true" v-on:click="removeTask(task)"></i>' + '</summary>' +
'<input class="subInput" type="text" placeholder="Tilføj til indøbsseddel" v-model="newSubTask" v-on:keyup.enter="addSubTask">' +
'</details>' +
'</div>',
computed: {
showInput: function() {
return !this.taskList.length
},
},
methods: {
//addTasks
//
addTask: function() {
var task = this.newTask.trim();
if (task) {
this.taskList.push({
text: task
});
this.newTask = "";
localStorage.setItem(STORAGE_KEY, JSON.stringify(this.taskList));
}
},
addSubTask: function() {
var task = this.newSubTask.trim();
if (task) {
this.subTaskList.push({
text: task
});
this.newSubTask = "";
this.$emit('addedtask', task);
localStorage.setItem(STORAGE_KEY, JSON.stringify(this.subTaskList));
}
},
//removeTasks
//
removeTask: function(task) {
var index = this.taskList.indexOf(task);
this.taskList.splice(index, 1);
},
},
});
new Vue({
el: "#madplan",
data: {
newTask: "",
taskList: [],
newSubTask: "",
subTaskList: [],
},
methods: {
acknowledgeAddedTask: function(cls, task) {
this.$data.subTaskList.push({
text: task,
class: "list-item " + cls
})
},
acknowledgeRemovedTask: function(task) {
this.$data.subTaskList = this.$data.subTaskList.filter(it => it.text != task.text)
},
removeSubTask: function(task) {
var index = this.subTaskList.indexOf(task);
this.subTaskList.splice(index, 1);
},
}
});
<section id="madplan" class="section-wrapper">
<section class="check-list">
<div id="mandag" class="dayWrapper">
<h1>Day One</h1>
<list-component
class="mandag"
v-on:addedtask='task => acknowledgeAddedTask("mandag", task)'
></list-component>
</div>
<div id="tirsdag" class="dayWrapper">
<h1>Day Two</h1>
<list-component
class="tirsdag"
v-on:addedtask='task => acknowledgeAddedTask("tirsdag", task)'
></list-component>
</div>
<ul id="indkobsseddel">
<h2>Shopping List</h2>
<li v-for="task in subTaskList" v-bind:key="task.text" :class="task.class">{{ task.text }}<i class="fa fa-times" aria-hidden="true" v-on:click="removeSubTask(task)"></i></li>
</ul>
</section>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<script src="https://unpkg.com/vue/dist/vue.js" charset="utf-8"></script>
To be clear, i will come with an example:
As it is now, if i post "Foo" and "Bar" to the "Day One" component and refresh the page, it will then post "Foo" and "Bar" to both "Day One" and "Day Two".
Essentially, i would like to be able to post for an example "Foo" to "Day One", "Bar" to "Day Two" and from there post "Hello World" to the Shopping List, and it would all be saved in the right places, instead of posting everything everywhere.
BTW: I'm a scrub at backend work.
To persist global state, you may use the plugin vue-persistent-state like this:
import persistentStorage from 'vue-persistent-storage';
const initialState = {
newTask: "",
taskList: [],
newSubTask: "",
subTaskList: [],
};
Vue.use(persistentStorage, initialState);
Now newTask, taskList, newSubTask and subTaskList is available as data in all components and Vue instances. Any changes will be stored in localStorage, and you can use this.taskList etc. as you would in a vanilla Vue app.
Your list component now becomes:
Vue.component('list-component', {
// data removed
// created removed
template:
'<div>' +
'<section class="prefetch">' +
'<input v-if="showInput" class="input typeahead" type="text" placeholder="Tilføj ret til madplanen" v-model="newTask" v-on:keyup.enter="addTask">' +
'</section>' +
'<details open v-for="task in taskList" v-bind:key="task.text" class="sub-list-item">' +
'<summary>{{ task.text }}<i class="fa fa-times" aria-hidden="true" v-on:click="removeTask(task)"></i>' + '</summary>' +
'<input class="subInput" type="text" placeholder="Tilføj til indøbsseddel" v-model="newSubTask" v-on:keyup.enter="addSubTask">' +
'</details>' +
'</div>',
computed: {
showInput: function() {
return !this.taskList.length
},
},
methods: {
//addTasks
//
addTask: function() {
var task = this.newTask.trim();
if (task) {
this.taskList.push({
text: task
});
this.newTask = "";
// localStorage.setItem not needed
}
},
addSubTask: function() {
var task = this.newSubTask.trim();
if (task) {
this.subTaskList.push({
text: task
});
this.newSubTask = "";
// $emit not needed, state is global and shared
// localStorage.setItem not needed
}
},
//removeTasks
//
removeTask: function(task) {
var index = this.taskList.indexOf(task);
this.taskList.splice(index, 1);
},
},
});
If you want to understand how this works, the code is pretty simple. It basically
adds a mixin to make initialState available in all Vue instances, and
watches for changes and stores them.
Disclaimer: I'm the author of vue-persistent-state.

Datatables .row() not working on reopening in bootstrap modal

I am creating the datatables in bootstrap modal and it is working fine for the first time modal opening, but when the modal is reopened, the datatable functions are not working.
$("#unitTypePopupModal .modal-body").empty();
var html = '<div class="container-fluid col-md-12">';
html += '<div class="row">';
html += '<div class="col-xs-10 col-xs-offset-1 col-sm-10 col-sm-offset-1 col-md-10 col-md-offset-1 col-lg-10 col-lg-offset-1">';
html += '<div class="panel panel-default">';
html += '<div class="panel-body">';
html += '<div class="text-center">';
html += '<img src="Images/add-circle.png" class="login" height="70" />';
html += '<h2 class="text-center">Units</h2>';
html += '<div class="panel-body">';
html += '<form id="unitTypePopupForm" name="unitTypePopupForm" role="form" class="form form-horizontal" method="post">';
//html += '<fieldset>';
html += '<div class="form-group">';
html += '<div id="table-container" style="padding:1%;">';
//if (UnitTypeData.length > 0) {
html += '<div class="table-responsive">';
html += '<table id="unitTypesList" class="table table-striped table-bordered table-hover" cellspacing="0" width="100%">';
html += '<thead>';
html += '<tr>';
html += '<th>' + '#' + '</th>';
html += '<th>' + 'UnitType Guid' + '</th>';
html += '<th>' + 'UnitType Name' + '</th>';
html += '<th>' + 'Unit List' + '</th>';
html += '<th>' + 'Operation' + '</th>';
html += '</tr>';
html += '</thead>';
html += '<tbody>';
for (var i = 0; i < UnitTypeData.length; i++) {
html += '<tr>';
html += '<td>' + '' + '</td>';
html += '<td>' + UnitTypeData[i].UnitTypeGuid + '</td>';
html += '<td>' + UnitTypeData[i].UnitTypeName + '</td>';
html += '<td>' + 'Unit' + '</td>';
html += '<td>' + '<i class="ui-tooltip fa fa-file-text-o" style="font-size: 22px;" data-original-title="View" title="View"></i>' + ' ' + '<i class="ui-tooltip fa fa-pencil-square-o" style="font-size: 22px;" data-original-title="Edit" title="Edit"></i>' + ' ' + '<i class="ui-tooltip fa fa-trash-o" style="font-size: 22px;" data-original-title="Delete" title="Delete"></i>' + '</td>';
html += '</tr>';
}
html += '</tbody>';
html += '</table>';
html += '</div>';
//} else {
// html += '<p>There is no Unit Type available to be displayed.</p>';
// }
html += '</div>';
html += '</div>';
//html += '</fieldset>';
html += '</form><!--/end form-->';
html += '</div>';
html += '</div>';
html += '</div>';
html += '</div>';
html += '</div>';
html += '</div>';
html += '</div>';
$("#unitTypePopupModal .modal-body").append(html);
$('#unitTypePopupModal').modal('show');
$("#unitTypePopupModal").on('show.bs.modal', function () {
$("#unitTypePopupModal .modal-body").empty();
//bootbox.alert(html);
});
$("#unitTypePopupModal").on('hiddden.bs.modal', function () {
//$("#unitTypePopupModal .modal-body").empty();
$(this).data('bs.modal', null);
});
unitTypeTableRelatedFunctions();
$('#unitTypePopupForm').validate({ // initialize plugin
ignore: ":not(:visible)",
rules: {
unitTypeName: {
required: true,
noSpace: true
}
},
messages: {
unitTypeName: {
required: "Please Enter the UnitType Name.",
noSpace: "UnitType Name cannot be empty."
}
},
//errorPlacement: function (error, element) {
// switch (element.attr("name")) {
// case "dpProgressMonth":
// error.insertAfter($("#dpProgressMonth"));
// break;
// default:
// //nothing
// }
//}
});
The following function is called
unitTypeTableRelatedFunctions = function () {
var unitTypeEditing = null;
var table = $('#unitTypesList').DataTable({
responsive: true,
pagingType: "full_numbers",
searchHighlight: true,
stateSave: true,
destroy: true,
//orderable: false,
columnDefs: [
{
targets: [0],
orderable: false,
searchable: false
},
{
className: 'never',//className:'hidden',
targets: [1],
visible: false,
orderable: false,
searchable: false
},
{
targets: [2],
orderable: true
},
{
targets: [3],
orderable: false
},
{
targets: [4],
orderable: false,
searchable: false,
//defaultContent: '<a class="viewWorkItemSubHeadBtn" onclick="editBtnClicked(this);"><i class="ui-tooltip fa fa-file-text-o" style="font-size: 22px;" data-original-title="View"></i></a> <a class="editWorkItemSubHeadBtn"><i class="ui-tooltip fa fa-pencil" style="font-size: 22px;" data-original-title="Edit"></i></a> <a class="deleteWorkItemSubHeadBtn"><i class="ui-tooltip fa fa-trash-o" style="font-size: 22px;" data-original-title="Delete"></i></a>'
}
],
order: [[2, 'desc']],
language: {
//"lengthMenu": "Display _MENU_ records per page",
lengthMenu: 'Display <select>' +
'<option value="5">5</option>' +
'<option value="10">10</option>' +
'<option value="15">15</option>' +
'<option value="20">20</option>' +
'<option value="25">25</option>' +
'<option value="30">30</option>' +
'<option value="35">35</option>' +
'<option value="40">40</option>' +
'<option value="45">45</option>' +
'<option value="50">50</option>' +
'<option value="100">100</option>' +
'<option value="-1">All</option>' +
'</select> records per page',
zeroRecords: "Nothing found - sorry",
info: "Showing page _PAGE_ of _PAGES_",
infoEmpty: "No records available",
infoFiltered: "(filtered from _MAX_ total records)"
},
pageLength: 5,
//select: {
// style: 'os',
// blurable: true
//},
dom: '<"top"<"pull-left"B><f>>rt<"bottom"i<"pull-left"l><p>><"clear">',
//dom: '<lf<t>ip>',
//dom: '<"wrapper"flipt>',
//dom: '<"top"i>rt<"bottom"flp><"clear">',
buttons: [
{
text: '+ Create New UnitType',
className: 'btn-success addNewUnitTypeBtn',
action: function (e, dt, node, config) {
e.preventDefault();
addNewUnitType();
}
}
]
});
table.on('order.dt search.dt', function () {
table.column(0, { search: 'applied', order: 'applied' }).nodes().each(function (cell, i) {
cell.innerHTML = i + 1;
});
}).draw();
$('#unitTypePopupModal').on('click', '#unitTypesList a.editUnitTypeBtn', function (e) {
e.preventDefault();
/* Get the row as a parent of the link that was clicked on */
var selectedRow = $(this).parents('tr')[0];
//var selectedRowIndex = table.row($(this).parents('tr')).index();
if (unitTypeEditing !== null && unitTypeEditing != selectedRow) {
/* A different row is being edited - the edit should be cancelled and this row edited */
restoreUnitTypeRowData(table, unitTypeEditing);
editUnitTypeRow(table, selectedRow);
unitTypeEditing = selectedRow;
addUnitTypeBtn.enable();
$('#unitTypePopupModal input#unitTypeName').focus();
}
//else if (nEditing == selectedRow && this.innerHTML == "Save") {
// /* This row is being edited and should be saved */
// saveRow(oTable, nEditing);
// nEditing = null;
// createBtnClicked = 0;
//}
else {
/* No row currently being edited */
editUnitTypeRow(table, selectedRow);
unitTypeEditing = selectedRow;
addUnitTypeBtn.enable();
$('#unitTypePopupModal input#unitTypeName').focus();
}
});
When edit button is clicked, the click event is fired and the following function is called:
editUnitTypeRow(table, selectedRow)
the function body is:
editUnitTypeRow = function (table, selectedRow) {
var selectedRowData = table.row(selectedRow).data();
var availableTds = $('>td', selectedRow);
availableTds[1].innerHTML = '<input type="text" id="unitTypeName" name="unitTypeName" placeholder="UnitType Name" class="form-control text-capitalize" value="' + selectedRowData[2] + '">';//UnitType
availableTds[2].innerHTML = '';
availableTds[3].innerHTML = '<i class="ui-tooltip fa fa-floppy-o" style="font-size: 22px;" data-original-title="Save" title="Save"></i>' + ' ' + '<i class="ui-tooltip fa fa-times-circle-o" style="font-size: 22px;" data-original-title="Cancel" title="Cancel"></i>'
}
In this function the table row data is not extracted properly.
I am getting the data when the modal is opened for the first time after page reload but if i open the modal second time the table.row() function is not fetching the row data, but the data is present in the row.
The html code is as follows:
<div id="unitTypePopupModal" class="modal fade" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-lg" style="overflow-y:initial !important;">
<div class="modal-content">
<div class="modal-header" style="display:none"></div>
<%--<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
<h1 class="text-center">What's My Password?</h1>
</div>--%>
<div class="modal-body" style="max-height:calc(100vh - 200px); overflow-y: auto;">
</div>
<div class="modal-footer">
<div class="col-md-12">
<button class="btn" data-dismiss="modal" aria-hidden="true">Close</button>
</div>
</div>
</div>
</div>
</div>
The clicks in popup modal works initially after page load but do not work after we re-open the popup modal.
Since you are dinamically adding html content, you need to delegate events in a different way
Code like this will execute only if object #selector is present on page at load time
$('#selector').on('click', function(){...etc...});
If you want to delegate event to dinamically added elements, you have to do like this:
$('#container').on('click', '#selector', function(){...etc...});
Where #container exist in DOM since page load. If you want to be 100% sure not to do mistake, just use $(document.body).on(...), but it will slow down a bit your code since the body is full of elements
EDIT after fiddle #12: I'm sorry but your code is way too crowded to easily find a solution. I tried to log some data and I see that when you close and reopen the modal, the event is triggered twice both when you click "Edit" and "Cancel" (I guess other buttons behave the same), so it is actually working correctly, but the secont time it is called, the unitTypeEditing is empty, that's why the input field isn't hiding..
I suggest you to switch to x-editable plugin, this should avoid any problem with restoring previous data and it will result in much less code to write.

Watin. how to show invinsible class

HTML code:
<div class="col-sm-9">
<input name="NewCardOrAccountNumber" class="form-control ui-autocomplete-input" id="NewCardOrAccountNumber" type="text" value="" autocomplete="off">
<span class="ui-helper-hidden-accessible" role="status" aria-live="polite"></span>
</div>
<div class="unvisible" id="clientInfoNew">
<div class="form-group">
<label class="col-sm-3 control-label">FIRST NAME</label>
<div class="col-sm-9" id="FnameNew"></div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">LAST NAME</label>
<div class="col-sm-9" id="LnameNew"></div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">BIRTH DATE</label>
<div class="col-sm-9" id="BirthDateNew"></div>
</div>
Watin code:
[TestMethod]
[TestCategory("Rimi Change card page")]
public void Rimi_4444_Change_Card_and_Assert()
{
//Web Address
using (IE ie = new IE(this.Rimi))
{
//IE ie = new IE(RimiChangeCard);
ie.BringToFront();
ie.ShowWindow(WatiN.Core.Native.Windows.NativeMethods.WindowShowStyle.Maximize);
ie.TextField(Find.ById("NewCardOrAccountNumber")).TypeText("9440385200600000020");
If I write card number from keyboard, the invisible class appear, and you can see FIRST NAME, LAST NAME and so on. But if I do this with watin, it does not appear, and you only see card number which you input. Its like hidden fields of information. I do not know how to make that I could see this fields when I input card number.
There would be a JavaScript function, which gets executed when you manually enter the data in the text field.Go through the Java Script functions on the same page which refer to that element using it's ID NewCardOrAccountNumber.
Refer to this link for sample application. Where msg_to is element, and has a KeyUp event associated. When that filed gets a , value, there is a div section inside which a `Subject' field is shown.
Similarly, after executing the TypeText, try to trigger related event mentioned in the Java Script event using Java script execution.
EDIT: I see that the javascript functions gets executed after bulr event is fired. This means the textbox field should loose the focus. Try the below options.
// 1. Try focusing out of control.
ie.TextField(Find.ById("NewCardOrAccountNumber")).TypeText("9440385200600000020");
ie.TextField(Find.ById("OldCardOrAccountNumber")).Click();
ie.WaitForComplete();
// 2. Try Using Send Keys method to tab out.
ie.TextField(Find.ById("NewCardOrAccountNumber")).TypeText("9440385200600000020");
System.Windows.Forms.SendKeys.SnedWait("{TAB}"); // Need to add System.Windows.Forms reference to the project.
I put image on the internet, so click on this link Image and you will see on first image how look page, second picture - what have to happen when you input card number (from keyboard), third - what happen when card namuber is input from watin (does not appear information about card).
HTML code:
<div class="ibox-content">
<br>
<div class="form-horizontal">
<div class="row">
<div class="col-md-5">
<div class="form-group">
<label class="col-sm-3 control-label">NEW CARD</label>
<input name="NewCardId" id="NewCardId" type="hidden" value="0" data-val-required="The NewCardId field is required." data-val-number="The field NewCardId must be a number." data-val="true">
<div class="col-sm-9"><span class="ui-helper-hidden-accessible" role="status" aria-live="polite"></span><input name="NewCardOrAccountNumber" class="form-control ui-autocomplete-input" id="NewCardOrAccountNumber" type="text" value="" autocomplete="off"></div>
</div>
<div class="unvisible" id="clientInfoNew">
<div class="form-group">
<label class="col-sm-3 control-label">FIRST NAME</label>
I maybe find what you looking for Sham, but I do not know how to use it :
<script type="text/javascript">
$(document).ready(function() {
var NewCardId = "#NewCardId";
var OldCardId = "#OldCardId";
var NewCardNumber = "#NewCardOrAccountNumber";
var OldCardNumber = "#OldCardOrAccountNumber";
$(NewCardNumber).autocomplete(
{
source: function(request, response) {
$.ajax({
url: '/LoyaltyWebApplication/Suggestion/GetCardSuggestions',
dataType: "json",
data: {
str: $(NewCardNumber).val()
},
success: function(data) {
response($.map(data, function(item) {
var label = "";
if (item.Fname != null) label += item.Fname;
if (item.Lname != null) label += " " + item.Lname;
if (label.trim() != '') label = " (" + label.trim() + ")";
return {
value: item.CardNumber,
label: item.CardNumber + label
}
}));
},
error: function(xhr, ajaxOptions, thrownError) {
alert(xhr.status);
alert(thrownError);
}
});
},
select: function(event, ui) {
getCardDetails($(NewCardNumber), $(NewCardId), 'newCardSegments', true);
$("#newCardSegments").hide();
$("#clientInfoNew").show();
},
minLength: 2
}).blur(function() {
getCardDetails($(NewCardNumber), $(NewCardId), 'newCardSegments', true);
});
$(OldCardNumber).autocomplete(
{
source: function(request, response) {
$.ajax({
url: '/LoyaltyWebApplication/Suggestion/GetCardSuggestions',
dataType: "json",
data: {
str: $(OldCardNumber).val()
},
success: function(data) {
response($.map(data, function(item) {
var label = "";
if (item.Fname != null) label += item.Fname;
if (item.Lname != null) label += " " + item.Lname;
if (label.trim() != '') label = " (" + label.trim() + ")";
return {
value: item.CardNumber,
label: item.CardNumber + label
}
}));
},
error: function(xhr, ajaxOptions, thrownError) {
alert(xhr.status);
alert(thrownError);
}
});
},
select: function(event, ui) {
getCardDetails($(OldCardNumber), $(OldCardId), 'oldCardSegments', false);
$("#oldCardSegments").hide();
},
minLength: 2
}).blur(function() {
getCardDetails($(OldCardNumber), $(OldCardId), 'oldCardSegments', false);
});
function getCardDetails(cardNumHolder, cardIdHolder, segmentTablePlace, isNew) {
$.getJSON('/LoyaltyWebApplication/LOV/SetId?lovType=ReplacementLOV&lovValue=' + cardNumHolder.val(), null,
function(data) {
$("#clientInfo" + ((isNew) ? "New" : "Old")).show();
if (cardNumHolder.val() == '') {
return;
}
var i;
for (i = 0; i < data.otherNames.length; i++) {
$("#" + data.otherValues[i] + (isNew ? "New" : "Old")).text(data.otherNames[i]);
}
cardIdHolder.val(data.Id);
$.getJSON('/LoyaltyWebApplication/Replacement/ClientSegmentsList?clientId=' + data.Id + "&no_cache=" + Math.random, function(data) {
$("#" + segmentTablePlace).find('tbody').empty();
if (data.length > 0) {
$.each(data, function(index) {
$("#" + segmentTablePlace).find('tbody').append("<tr><td>" + data[index].SegmentCode + "</td><td>" + data[index].SegmentName + "</td></tr>");
});
$("#" + segmentTablePlace).show();
}
});
});
}
$("#resetVal").click(function() {
$("#NewCardOrAccountNumber").attr("value", "");
$("#NewCardOrAccountNumber").val("");
$("#NewCardId").attr("value", "");
$("#NewCardId").val("");
$("#clientInfoNew").hide();
$("#OldCardOrAccountNumber").attr("value", "");
$("#OldCardOrAccountNumber").val("");
$("#OldCardId").attr("value", "");
$("#OldCardId").val("");
$("#clientInfoOld").hide();
return false;
});
});
</script>