I am working on this Laravel Social Network Script and it uses Vue 1.0.26.
My objetive is to bind the class connected or disconnected according to the user status which is true or false. I created a Laravel API to get the user status:
Controller (Laravel):
// $id = User id - https://www.mysite.uy/api/v1/status/{userid}
public function getUserStatus($id)
{
$active = false;
$open_session = \App\OpenSession::where('user_id', $id)->first();
if($open_session) {
if(Carbon::parse($open_session->created_at)->diffInSeconds(Carbon::parse($open_session->expires)) < 86400 && $open_session->active > 0) {
$active = true;
}
}
return response()->json($active);
}
This works fine from the URL, it returns true or false, but then here is some view.blade.php:
<li class="list-group-item" v-for="conversation in conversations.data" v-if="conversation.user"> <!-- Conversaciones -->
<a href="#" #click.prevent="showChatBox(conversation)">
<div class="media">
<div class="media-left">
<img v-bind:src="conversation.user.avatar" alt="images">
</div>
<div class="media-body">
<h4 class="media-heading">
#{{ conversation.user.name }}
<i v-bind:class="getUserStatus(conversation.user.id)"></i> <!-- <-- look at here -->
</h4>
<span class="pull-right active-ago" v-if="message">
<time class="microtime" datetime="#{{ message.created_at }}" title="#{{ message.created_at }}">
#{{ message.created_at }}
</time>
</span>
</div>
</div>
</a>
</li>
This block is being displayed as <i></i> on the HTML when using the getUserStatus() method on computed:
<h4 class="media-heading">
#{{ conversation.user.name }}
<i v-bind:class="getUserStatus(conversation.user.id)"></i> <!-- <-- look at here -->
</h4>
And this displays <i class="status disconnected"></i> even when the response.data is true and I check it with console.log(response.data == true):
<h4 class="media-heading">
#{{ conversation.user.name }}
<i v-bind:class="['status', getUserStatus(conversation.user.id) ? 'connected' : 'disconnected' ]"></i>
</h4>
And here is the Vue stuff:
data: {
status: {
on: 'connected',
off: 'disconnected'
},
},
created: {
...
},
methods: {
getUserStatus: function(userid)
{
this.$http.post(base_url + 'api/v1/status/' + userid).then(function(response) {
if(response.data == 'true') {
return true;
} else {
return false;
}
});
},
},
computed: {
/*getUserStatus: function(userid)
{
this.$http.post(base_url + 'api/v1/status/' + userid).then(function(response) {
return {
status: true,
connected: response.data == 'true',
disconnected: response.data == 'false'
}
});
}*/
}
I only have one of the methods "working", but I switch them. I am reading this guide but I get this is for Vue 2.
Ok, I made it work by adding a default variable in data and changing it later:
data: {
...
userStatus: {},
}
And in the method. As you can see I made some irrelevant changes on the Controller side.
methods: {
getUserStatus: function(userid)
{
var userStatus = this.$http.post(base_url + 'api/v1/status/' + userid).then(function(response) {
this.userStatus = JSON.parse(response.data);
});
return this.userStatus.status;
},
...
}
And in the HTML side I use:
<h4 class="media-heading">
#{{ conversation.user.name }}
<i class="status connected" v-bind:class="[ 'status', getUserStatus(conversation.user.id) ? 'connected' : 'disconnected' ]"></i>
</h4>
Now I have a different issue: The function seems to run repeteadly so the status are constantly changing. All the other functions runs once.
Related
I am developing an application and i want to upload a picture using Ajax with symfony 3.4. Sorry if i am missing anything because i am new to AJAX. I am following the step from https://codepen.io/dsalvagni/pen/BLapab
I am getting the 200 response from symfony but the files doesnt upload.
Entity:
/**
* #ORM\Column(type="string", length=255)
* #var string
*/
private $image;
/**
* #Vich\UploadableField(mapping="profile_image", fileNameProperty="image")
* #var File
*/
private $imageFile;
Here is my controller:
public function testAction(Request $request)
{
$testEntry = new Test();
$form = $this->createForm(TestType::class, $testEntry);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$file = $testEntry->getImageFile();
$fileName = md5(uniqid()) . '.' . $file->guessExtension();
$photoDir = $this->container->getParameter('kernel.root_dir') . '/../web/uploads/images';
$file->move($photoDir, $fileName);
$testEntry->setImage($fileName);
$em = $this->getDoctrine()->getManager();
$em->persist($testEntry);
$em->flush();
if ($request->isXmlHttpRequest()) {
return new JsonResponse(array('message' => 'Success!', 'success' => true), 200);
}
if ($request->isMethod('POST')) {
return new JsonResponse(array('message' => 'Invalid form', 'success' => false), 400);
}
return $this->redirect($this->generateUrl('homepage'));
}
return $this->render('#Alumni/Default/test.html.twig',
['form' => $form->createView()]);
}
and here is my html.twig
{{ form_start(form) }}
{{ form_errors(form) }}
{{ form_row(form.name) }}
<div class="profile">
<div class="photo">
{{ form_widget(form.imageFile, {'attr': {'class': 'file-upload'}}) }}
<div class="photo__helper">
<div class="photo__frame photo__frame--circle">
<canvas class="photo__canvas"></canvas>
<div class="message is-empty">
<p class="message--desktop">Drop your photo here or browse your computer.</p>
<p class="message--mobile">Tap here to select your picture.</p>
</div>
<div class="message is-loading">
<i class="fa fa-2x fa-spin fa-spinner"></i>
</div>
<div class="message is-dragover">
<i class="fa fa-2x fa-cloud-upload"></i>
<p>Drop your photo</p>
</div>
<div class="message is-wrong-file-type">
<p>Only images allowed.</p>
<p class="message--desktop">Drop your photo here or browse your computer.</p>
<p class="message--mobile">Tap here to select your picture.</p>
</div>
<div class="message is-wrong-image-size">
<p>Your photo must be larger than 350px.</p>
</div>
</div>
</div>
<div class="photo__options hide">
<div class="photo__zoom">
<input type="range" class="zoom-handler">
</div><i class="fa fa-trash"></i>
</div>
<button type="button" id="uploadBtn">Upload</button>
</div>
</div>
{{ form_row(form.submit, { 'label': 'Submit me' }) }}
{{ form_end(form) }}
here is my route
test:
path: /test
defaults: {_controller: AlumniBundle:Default:test}
here is my js
$(function() {
/**
* DEMO
*/
var p = new profilePicture('.profile', null,
{
imageHelper: true,
onRemove: function (type) {
$('.preview').hide().attr('src','');
},
onError: function (type) {
console.log('Error type: ' + type);
}
});
$('#uploadBtn').on('click', function() {
var image = p.getAsDataURL();
$.post("/test", { image: image });
});
and i am getting 200 response but i cannot locate the file:
Many thanks in advance for your help
It won't upload because your function isn't sending any file.
It's just sending the file name at most.
The Function
function ajaxSubmit(node) {
$.ajax({
type: node.attr("method"), // Method on form tag
url: node.attr("action"), // Action on form tag
enctype: "multipart/form-data", //needed to upload files
data: new FormData(node[0]), // The form content
processData: false,
contentType: false,
cache: false
}).done(function(response, status, xhr) {
console.info(response);
}).fail(function(request, status, error) {
console.error(request);
console.error(status);
console.error(error);
});
}
The Action
$(body).on("submit", ".ajs", function(e) {
e.preventDefault(); // Prevent default HTML behavior
ajaxSubmit($(this)); //Handle submit with AJAX
});
The View
// Add 'ajs' class to forms that should be submited with AJAX
{{ form_start(form, {'class': 'ajs'}) }}
I am working on a page in a Laravel 5.7 application, that has a series of VueJS child components "Prospect" wrapped within a VueJS parent component "Prospects". The child components are table rows that each have a button to delete the row/child. The parent component has a button that deletes multiple child components.
However I am unsure as to how to get the parent component function to call the child component functions to delete the children that have been marked for deletion.
Deleting the child components one at a time from within the child component itself works fine. However I am trying to call the same Child component delete function multiple times from the loop within the parent component function deleteSelectedProspects().
From within deleteSelectedProspects() i need to access the index assigned to the child component, in order to reference the correct row in the $refs array.
How can I access the index of the child component in order to properly reference it in the $refs array inside the selectedBoxes.forEach loop?
Source code for the blade view that contains the Prospects parent component:
#extends('layouts.admin')
#section('content')
<section id="widget-grid">
<div class="row">
<article class="col-xs-12 col-sm-12 col-md-12 col-lg-12 sortable-grid ui-sortable">
<prospects inline-template class="">
<div>
<div class="jarviswidget jarviswidget-sortable" id="wid-id-1" data-widget-editbutton="false" data-widget-deletebutton="false">
<header class="ui-sortable-handle">
<div class="widget-header">
<h2><i class="fa fa-list text-orange"></i> Prospects/Leads List</h2>
</div>
<button class="btn btn-sm btn-primary d-none" #click="markSelectedProspectsContacted" id="btnMarkSelectedProspectsContacted">
<span class="fa fa-check-square-o"></span> Mark Selected as Contacted
</button>
<button class="btn btn-sm btn-danger d-none" #click="deleteSelectedProspects" id="btnDeleteSelectedProspects"><span class="fa fa-trash"></span> Delete Selected</button>
</header>
<!-- widget div-->
<div role="content">
<!-- widget content -->
<div class="widget-body">
<table id="prospectsTable" class="table table-striped table-bordered table-hover">
<thead>
<tr>
<th>
<label class="vcheck">
<input type="checkbox" id="toggleCheckAllProspects" value="1" /> <span></span>
</label>
</th>
<th>Name</th>
<th>Food Category</th>
<th>Contact</th>
<th>Admin Comments</th>
<th></th>
</tr>
</thead>
<tbody>
<tr is="prospect"
v-for="(prospect, index) in prospects"
:prospect="prospect"
:index="index"
:key="prospect.id"
ref="refIds"
#delete-prospect="removeProspect()"
#update-contacted="updateContacted(index)">
</tr>
</tbody>
</table>
</div>
<!-- end widget content -->
</div>
</div>
<!-- end jarviswidget div -->
</div>
</prospects>
</article>
</div>
</section>
#endsection
#push('js-block')
<script>
$.widget( "ui.tabs", $.ui.tabs, {
_getPanelForTab: function( tab ) {
var id = $( tab ).attr( "aria-controls" );
return $( this._sanitizeSelector( "#" + id ) );
}
});
$(function() {
$('#prospectTabs').tabs({
create: removeLoader($('#prospectTabs'))
});
$('#prospectTabsAdd').click(function() {
$('#add-tab').removeClass('d-none');
});
$('#toggleCheckAllProspects').click(function() {
$('.prospect-check').prop('checked', ($(this).is(':checked') ? true : false));
});
});
</script>
#endpush
Source code for the parent component "prospects":
<script>
export default {
data: function() {
return {
formProspect: {
inputs: {
name: '',
food_cat_id: '',
phone: '',
website: '',
contact_fname: '',
contact_lname: '',
contact_title: '',
address: '',
address2: '',
city: '',
state_id: '',
zip: '',
response_notes: ''
},
headerText: "<i class='fa fa-plus-square-o text-orange'></i> Add Prospect / Lead",
btnText: "<i class='fa fa-plus-square-o'></i> Add Prospect / Lead",
errors: false
},
formSearchProspect: {
inputs: {
keywords: '',
contacted: '',
location: '',
radius: 5
}
},
formProspectMode: 'add',
prospects: []
}
},
computed: {
},
watch: {
formProspectMode: function(newMode) {
switch(newMode) {
case 'add':
this.formProspect.headerText = "<i class='fa fa-plus-square-o text-orange'></i> Add Prospect / Lead";
this.formProspect.btnText = "<i class='fa fa-plus-square-o'></i> Add Prospect / Lead";
this.clearFormProspect();
break;
case 'edit':
this.formProspect.headerText = "<i class='fa fa-edit text-orange'></i> Edit Prospect / Lead";
this.formProspect.btnText = "<i class='fa fa-save'></i> Save Prospect / Lead";
break;
}
}
},
methods: {
fetchProspects() {
var self = this;
$.get('/prospect/fetch', function(r) {
if(r.successMsg) {
self.prospects = r.prospects;
}
})
},
searchProspects() {
var self = this;
var params = {
'keywords' : this.formSearchProspect.inputs.keywords,
'contacted' : this.formSearchProspect.inputs.contacted
};
if(this.formSearchProspect.inputs.location) {
params.location = this.formSearchProspect.inputs.location;
params.radius = this.formSearchProspect.inputs.radius;
}
$.get('/prospect/search', params, function(r) {
if(r.successMsg) {
self.prospects = r.prospects;
}
})
},
addProspect() {
var self = this;
$.ajax({
type: "POST",
dataType: "json",
async: false,
url: "/prospect/add",
data: {
name: this.formProspect.inputs.name,
food_cat_id: this.formProspect.inputs.food_cat_id,
phone: this.formProspect.inputs.phone,
website: this.formProspect.inputs.website,
contact_fname: this.formProspect.inputs.contact_fname,
contact_lname: this.formProspect.inputs.contact_lname,
contact_title: this.formProspect.inputs.contact_title,
address: this.formProspect.inputs.address,
address2: this.formProspect.inputs.address2,
city: this.formProspect.inputs.city,
state_id: this.formProspect.inputs.state_id,
zip: this.formProspect.inputs.zip,
response_notes: this.formProspect.inputs.response_notes
}, success(r) {
if(r.successMsg) {
var newProspect = self.formProspect.inputs;
newProspect.id = r.newId;
newProspect.state = r.state;
newProspect.food_cat = r.food_cat;
console.log(newProspect);
self.prospects.push(Object.assign({}, newProspect));
self.clearFormProspect();
} else if(r.errors) {
self.formProspect.errors = r.errors;
}
}
});
},
saveProspect() {
var self = this;
$.post('/prospect/edit', {
id: this.formProspect.inputs.id,
name: this.formProspect.inputs.name,
food_cat_id: this.formProspect.inputs.food_cat_id,
phone: this.formProspect.inputs.phone,
website: this.formProspect.inputs.website,
contact_fname: this.formProspect.inputs.contact_fname,
contact_lname: this.formProspect.inputs.contact_lname,
contact_title: this.formProspect.inputs.contact_title,
address: this.formProspect.inputs.address,
address2: this.formProspect.inputs.address2,
city: this.formProspect.inputs.city,
state_id: this.formProspect.inputs.state_id,
zip: this.formProspect.inputs.zip,
response_notes: this.formProspect.inputs.response_notes
}, function(r) {
if(r.successMsg) {
var savedProspect = self.prospects.filter(prospect => prospect.id == self.formProspect.inputs.id)[0];
savedProspect = Object.assign({}, self.formProspect.inputs);
self.formProspectMode = "add";
} else if(r.errors) {
self.formProspect.errors = r.errors;
}
});
},
removeProspect(index) {
this.prospects.splice(index, 1);
},
clearFormProspect() {
this.formProspect.inputs = {};
this.formProspect.errors = false;
},
deleteSelectedProspects() {
var self = this;
var selectedBoxes = self.prospects.filter(prospect => prospect.selected);
$.SmartMessageBox({
title: "<i class='fa fa-trash text-orange-dark'></i> Delete (" + selectedBoxes.length + ") Selected Prospects",
content: "Are you sure you want to delete the selected leads?",
buttons: "[No][Yes]"
}, function(e) {
if("Yes" == e) {
selectedBoxes.forEach(function(p) {
var lostChild = self.prospects.filter(prospect => prospect.id == p.id);
// HOW CAN I ACCESS THE INDEX OF THE LOST CHILD TO REFERENCE IT IN THE $refs ARRAY BELOW
// HOW CAN I ACCESS THE INDEX OF THE LOST CHILD TO REFERENCE IT IN THE $refs ARRAY BELOW
//self.$refs.refIds[lostChildIndex].deleteProspect();
});
}
});
},
markSelectedProspectsContacted() {
var self = this;
var selectedBoxes = self.prospects.filter(prospect => prospect.selected);
$.SmartMessageBox({
title: "<i class='fa fa-trash text-orange-dark'></i> Mark (" + selectedBoxes.length + ") Selected Prospects as Contacted",
content: "Are you sure you want to mark the selected leads as contacted?",
buttons: "[No][Yes]"
}, function(e) {
if("Yes" == e) {
selectedBoxes.forEach(function(p) {
/*
mark contacted
*/
});
}
});
},
updateRadiusSearchDiv() {
if($('#searchLocation').val()) {
$('#radiusSearchDiv').removeClass('d-none');
} else {
$('#radiusSearchDiv').addClass('d-none');
}
}
},
mounted() {
this.fetchProspects();
setTimeout(function() {
$('#prospectsTable').DataTable({
"columnDefs": [
{ "orderable": false, "targets": [0,4,5] }
],
"language": {
"search": "Filter Results:"
},
"dom": "ftilp"
});
$('#prospectsTable > thead > tr th:first-child').removeClass('sorting_asc');
}, 500);
$('#btnDeleteSelectedProspects, #btnMarkSelectedProspectsContacted').removeClass('d-none');
}
}
</script>
Source code for the child component "prospect":
<template>
<transition name="fade">
<tr v-if="show">
<td class="text-center align-middle">
<label class="vcheck">
<input type="checkbox" class="prospect-check" value="1" v-model="prospect.selected" /> <span></span>
</label>
</td>
<td>
<div>{{ prospect.name }}</div>
<div v-show="prospect.contacted" class="label label-success"><span class="fa fa-check-square-o"></span> Contacted!</div>
</td>
<td>{{ prospect.food_cat.title }}</td>
<td>{{ prospect.contact_fname }}<span v-if="prospect.contact_lname"> {{ prospect.contact_lname }}</span><span v-if="prospect.contact_title">, {{ prospect.contact_title }}</span></td>
<td>{{ prospect.response_notes }}</td>
<td class="text-right align-middle">
<button class="btn btn-primary" #click="updateContacted" v-show="!prospect.contacted"><span class="fa fa-check-square-o"></span> Mark As Contacted</button>
<button class="btn btn-primary" #click="editProspect"><span class="fa fa-edit"></span> Edit Lead</button>
<button class="btn btn-danger" #click="removeProspect"><span class="fa fa-trash"></span> Delete Lead</button>
</td>
</tr>
</transition>
</template>
<script>
export default {
props: ['prospect', 'index'],
data: function() {
return {
prospectData: this.prospect,
show: true,
prospectIndex: this.index
}
},
methods: {
editProspect() {
this.$parent.formProspect.inputs = this.prospectData;
this.$parent.formProspectMode = "edit";
$('#prospectTabs').tabs('option', 'active', 1);
$('#add-tab').removeClass('d-none');
window.scrollTo({ top: 0, behavior: 'smooth' });
},
updateContacted() {
var self = this;
$.SmartMessageBox({
title: "<i class='fa fa-trash text-orange-dark'></i> Mark as Contacted?",
buttons: "[No][Yes]"
}, function(e) {
if("Yes" == e) {
$.post('/prospect/updateContacted/' + self.prospectData.id, function(r) {
if(r.successMsg) {
self.$emit('update-contacted');
}
});
}
});
},
removeProspect() {
var self = this;
$.SmartMessageBox({
title: "<i class='fa fa-trash text-orange-dark'></i> Delete Prospect/Lead",
content: "Are you sure you want to delete this prospect/lead?",
buttons: "[No][Yes]"
}, function(e) {
if("Yes" == e) {
self.deleteProspect();
}
});
},
deleteProspect() {
var self = this;
$.post('/prospect/delete/' + self.prospectData.id, function(r) {
if(r.successMsg) {
self.show = false;
self.$emit('delete-prospect');
}
});
}
},
mounted() {
}
}
</script>
Your code is a little bit confused so this is just an idea that should work well, though. You should not touch refs, you just need to change the data model. Remove from the data movel and components will remove themselves.
In the parent component add a method called, say, checkProspect like
data: {
...
checkedProspects: []
},
methods: {
checkProspect(prospectId){
checkedProspects.push(prospectId);
},
deleteCheckedProspects(){
// Remove from the model by checkedProspects ids
}
}
Then add the handler:
<tr is="prospect"
v-for="(prospect, index) in prospects"
:prospect="prospect"
:index="index"
:key="prospect.id"
ref="refIds"
#checkProspect="checkProspect"
#delete-prospect="removeProspect()"
#update-contacted="updateContacted(index)">
In the child component, when you click to check it, emit the event:
this.$emit('checkProspect', this.id);
I'm new to Vue.js but trying to get to grips with it. So far it has gone well and I've got quite far but I am stuck extending a parents template.
I am trying to make dashboard widgets that extend a default widget layout (in Boostrap). Please note that the below code is using Vue, Require, Underscore & Axios.
Parent file - _Global.vue
<template>
<div class="panel panel-default">
<div class="panel-heading">
<b>{{ widgetTitle }}</b>
<div class="pull-right">
<a href="#" v-on:click="toggleMinimized"
v-bind:title="(isMinimized ? 'Show widget' : 'Hide widget')">
<i class="fa fa-fw" v-bind:class="isMinimized ? 'fa-plus' : 'fa-minus'"></i>
</a>
</div>
</div>
<div class="panel-body" v-if="!isMinimized">
<div class="text-center text-muted" v-if="!isLoaded">
<i class="fa fa-spin fa-circle-o-notch"></i><br />
</div>
<parent v-if="isLoaded">
<!-- parent content should appear here when loaded -->
</parent>
</div>
</div>
</template>
<script>
export default {
// setup our widget props
props: {
'minimized': {
'default': false,
'required': false,
'type': Boolean
}
},
// define our data
data: function () {
return {
widgetTitle: 'Set widget title in data',
isLoaded: false,
isMinimized: this.$props.minimized
}
},
// when vue is mounted, open our widget
mounted: function () {
if(!this.isMinimized) {
this.opened();
}
},
// define our methods
methods: {
// store our widget state to database
storeWidgetState: function () {
// set our data to send
let data = {
'action' : 'toggleWidget',
'widget' : this.$options._componentTag,
'state' : !this.isMinimized
};
// post our data to our endpoint
axios.post(axios.endpoint, data);
},
// toggle our minimized data
toggleMinimized: function (e) {
// prevent default
e.preventDefault();
// toggle our minimized state
this.isMinimized = !this.isMinimized;
// trigger opened if we aren't minimized
if(!this.isMinimized) this.opened();
// save our widget state to database
this.storeWidgetState();
},
// triggered when opened from being minimized
opened: function () {
console.log('opened() method is where all widget logic should be placed');
}
}
}
</script>
Child file - Example.vue
Should extend _Global.vue using mixins and then display content within .panel-body
<template>
<div>
I want this content to appear inside the .panel-body div
{{ content }}
<img v-bind:src="image.src" v-bind:alt="image.alt"
v-if="image.src" class="img-responsive" style="margin: 0 auto" />
</div>
</template>
<script>
// import our widgets globals
import Global from './_Global.vue'
export default {
components: {
'parent': {
// what can I possibly put here??
}
},
// use our global mixin for all widgets
mixins: [Global],
// setup our methods for this widget
methods: {
opened: _.debounce(function () {
// make sure this can only be opened once
if(this.hasBeenOpened) return;
this.hasBeenOpened = true;
// temporarily allow axios to make external requests
let axiosHeaders = axios.defaults.headers.common;
let vm = this;
axios.defaults.headers.common = {};
axios.get('https://yesno.wtf/api')
.then(function (res) {
// set our content
vm.content = null;
// set our image content
vm.image.src = res.data.image;
vm.image.alt = res.data.answer;
})
.catch(function (err) {
// set our error text
vm.content = String(err);
})
.then(function () {
// this will always hit..
vm.isLoaded = true;
});
// restore our axios headers for security
axios.defaults.headers.common = axiosHeaders;
}, 300)
},
// additional data
data: function () {
return {
// set our widgets title
widgetTitle: 'Test title',
// logic for the specific widget
hasBeenOpened: false,
content: 'Loaded and ready to go...',
image: {
src: false,
alt: null
}
};
},
}
</script>
Currently my parent template is just completely overwriting my child view. The only way I can get it to work is by explicitly defining the template parameter inside components -> parent: {} but I don't want to have to do that...?
Ok, thanks to Gerardo Rosciano for pointing me the in right direction. I've used to slots to come up with an eventual solution. We then access parent methods and data attributes just to get everything working as it should.
Example.vue - our example widget
<template>
<div>
<widget-wrapper>
<span slot="header">Example widget</span>
<div slot="content">
<img v-bind:src="image.src" v-bind:alt="image.alt"
v-if="image.src" class="img-responsive" style="margin: 0 auto" />
{{ content }}
</div>
</widget-wrapper>
</div>
</template>
<script>
// import our widgets globals
import WidgetWrapper from './_Widget.vue'
export default {
// setup our components
components: {
'widget-wrapper': WidgetWrapper
},
// set our elements props
props: {
'minimized': {
'type': Boolean,
'default': false,
'required': false
}
},
// setup our methods for this widget
methods: {
loadContent: _.debounce(function () {
// make sure this can only be opened once
if(this.hasBeenOpened) return;
this.hasBeenOpened = true;
// temporarily allow axios to make external requests
let axiosHeaders = axios.defaults.headers.common;
let vm = this;
axios.defaults.headers.common = {};
axios.get('https://yesno.wtf/api')
.then(function (res) {
// set our content
vm.content = null;
// set our image content
vm.image.src = res.data.image;
vm.image.alt = res.data.answer;
})
.catch(function (err) {
// set our error text
vm.content = String(err);
})
.then(function () {
// this will always hit..
vm.isLoaded = true;
});
// restore our axios headers for security
axios.defaults.headers.common = axiosHeaders;
}, 300)
},
// additional data
data: function () {
return {
// global param for parent
isLoaded: false,
// logic for the specific widget
hasBeenOpened: false,
content: 'Loaded and ready to go...',
image: {
src: false,
alt: null
}
};
},
}
</script>
_Widget.vue - our base widget that gets extended
<template>
<div class="panel panel-default">
<div class="panel-heading">
<b><slot name="header">Slot header title</slot></b>
<div class="pull-right">
<a href="#" v-on:click="toggleMinimized"
v-bind:title="(minimized ? 'Show widget' : 'Hide widget')">
<i class="fa fa-fw" v-bind:class="minimized ? 'fa-plus' : 'fa-minus'"></i>
</a>
</div>
</div>
<div class="panel-body" v-if="!minimized">
<div class="text-center text-muted" v-if="!isLoaded">
<i class="fa fa-spin fa-circle-o-notch"></i><br />
Loading...
</div>
<div v-else>
<slot name="content"></slot>
</div>
</div>
</div>
</template>
<script>
export default {
// get loaded state from our parent
computed: {
isLoaded: function () {
return this.$parent.isLoaded;
}
},
// set our data element
data: function () {
return {
minimized: false
}
},
// when the widget is mounted, trigger open state
mounted: function () {
this.minimized = this.$parent.minimized;
if(!this.minimized) this.opened();
},
// methods to manipulate our widget
methods: {
// save our widget state to database
storeWidgetState: function () {
// set our data to send
let data = {
'action' : 'toggleWidget',
'widget' : this.$parent.$options._componentTag,
'state' : !this.minimized
};
// post this data to our endpoint
axios.post(axios.endpoint, data);
},
// toggle our minimized state
toggleMinimized: function (e) {
// prevent default
e.preventDefault();
// toggle our minimized state
this.minimized = !this.minimized;
// trigger opened if we aren't minimized
if(!this.minimized) this.opened();
// save our widget state to database
this.storeWidgetState();
},
// when widget is opened, load content
opened: function () {
// make sure we have a valid loadContent method
if(typeof this.$parent.loadContent === "function") {
this.$parent.loadContent();
} else {
console.log('You need to define a loadContent() method on the widget');
}
}
}
}
</script>
I'm working on a project that need to allow users to add/remove tags on images.
There is grid view, single view and mixed view.
The grid view displays image thumbs in a grid,
Single view displays images one by one
and the mixed view has the grid in the background, and a single images in the front (a zoom in feature).
All those views have a footer which contains the tags that can be applied to an image. However, the grid has its own footer, while the single and mixed views share theirs.
Here is the HTML side code for those :
<section id="noRefContainer" class="photosWrapper photosWrapper-cq" style="display: block"> <!--ng-controller="gridController">-->
<div class="images-cq__wrapper" ng-show="displayStyle.style == 'grid' || displayStyle.style == 'both'">
<div class="images-cq__item" ng-repeat="photo in displayedPhotos">
<div ng-class="{active: photo.selected}">
<label for="{{photo.uuid}}">
<div class="img-cq">
<img ng-src="{{photo.thumbPath100}}" alt="Alternate Text" ng-click="selectionEvent({value: photo.uuid, event: $event, index: $index})" />
zoom
</div>
<p>
{{photo.title}}
</p>
</label>
</div>
</div>
<div class="images-cq__footer open">
<p>
<span>Tagger les</span>
<strong>{{selectedPhotos.length}}</strong>
<span>éléments sélectionnés</span>
</p>
<div class="images-cq__dropdown top">
...
<ul>
<li>Sélectionner toutes les images</li>
<li>Désélectionner toutes les images</li>
</ul>
</div>
<div class="images-cq__tags">
<ul>
<li ng-repeat="tag in tags">
{{tag.name}}
</li>
</ul>
</div>
<small>Attention, ceci effacera les précédents tags.</small>
</div>
</div>
<div ng-class="{'images-cq__lightbox': displayStyle.style == 'both', 'images-cq__wrapper': displayStyle.style == 'single', single: displayStyle.style == 'single'}" ng-show="displayStyle.style == 'both' || displayStyle.style == 'single'">
<div class="images-cq__carousel">
<a href="" class="images-cq__carouselclose" ng-click="zoomClose()" ng-show="displayStyle.style == 'both'">
Close
</a>
<div class="images-cq__carouselcontent" id="carousel">
</div>
<div class="images-cq__carouselfooter">
<div class="images-cq__tags">
<ul>
<li ng-repeat="tag in tags">
{{tag.name}}
</li>
</ul>
</div>
</div>
</div>
</div>
</section>
And the app.js side code :
$scope.tags = [];
$scope.tags = [{ name: 'CQ:OK', count: 0, status: '', value: 'CQ:OK' },
{ name: 'CQ:OK_NO_ZOOM', count: 0, status: '', value: 'CQ:OK_NO_ZOOM' },
{ name: 'CQ:KO', count: 0, status: '', value: 'CQ:KO' },
{ name: 'Chromie', count: 0, status: '', value: 'Chromie' },
{ name: 'Détourer', count: 0, status: '', value: 'Détourer' },
{ name: 'Nettoyer_redresser', count: 0, status: '', value: 'Nettoyer_redresser' },
{ name: 'Interne', count: 0, status: '', value: 'Interne' },
{ name: 'Otsc', count: 0, status: '', value: 'Otsc' }];
$scope.zoomTagSelectionEvent = function (tag) {
var photo = $scope.carousel.settings.images[$scope.carousel.settings.currentImage];
if ($scope.hasTag(photo, tag.value)) {
$scope.removeTag(photo, tag.value);
}
else {
$scope.addTag(photo, tag.value);
}
$scope.updateTagStatus(tag.value);
}
$scope.tagSelectionEvent = function (tag) {
if ($scope.allHaveTag($scope.selectedPhotos, tag.value)) {
$scope.allRemoveTag($scope.selectedPhotos, tag.value);
}
else {
$scope.allAddTag($scope.selectedPhotos, tag.value);
}
$scope.updateTagStatus(tag.value);
}
$scope.updateAllTagStatus = function () {
angular.forEach($scope.tags, function (value, key) {
$scope.updateTagStatus(value.value);
});
}
$scope.updateTagStatus = function (tag) {
var currentTag = $scope.getTag(tag);
if ($scope.displayStyle.style == 'grid')
{
var tagged = $scope.countTagged($scope.selectedPhotos, tag);
if (tagged == 0) {
currentTag.status = 'none';
}
else if (tagged < $scope.selectedPhotos.length) {
currentTag.status = 'selected';
}
else {
currentTag.status = 'active';
}
}
else {
if ($scope.carousel.settings.currentImage !== false)
{
var photo = $scope.carousel.settings.images[$scope.carousel.settings.currentImage];
var res = $scope.hasTag(photo, tag);
if (res) {
currentTag.status = 'active';
}
else {
currentTag.status = 'none';
}
}
}
console.log('tag ' + tag + ' status updated');
}
Each time a tag is applied to an image, the tag status is updated, which should update the ng-class expression result. The only part that gets properly updated is the grid footer. That is shared between single/mixed view updates only when the view is shown.
As for what i tried to fix this, I've tried using $scope.apply() after each call for tag update, tried placing at the end of the updateTagStatus function. I also tried changing the expressions (class names with/without quotes, setting class to the tag status...), which all worked only for the grid footer, but not for the other. I also checked that the statuses were properly updated, the only problem is in the updating of the display.
Please help.
Update :
I am sorry for not answering in here, there was a huge list of evolutions for the project in a short amount of time so the code causing this problem is no more, which also removes the problem altogether. I was busy working on the project and forgot to update this.
However, thank you for taking the time to come here and try to help me.
I am not sure what i should do in a situation such a this.
For the first time it looks like var currentTag = $scope.getTag(tag); this line is the culprit. It might not be returning proper angular object for tag.
Or you can try using $apply function properly.
i.e. user $apply for every update statement like:
$scope.$apply(function () { currentTag.status = 'selected'; });
$scope.$apply(function () { currentTag.status = 'none'; });
I am trying to update a program for updating and inserting into mongodb, but I keep getting an error. Let me paste the relevant code here, and I hope to get some help.
PosProductVariant.js
module.exports = {
schema : true,
attributes: {
// unique name identifier
name : { type: 'text', unique : true , required : true },
product : { model : 'PosCustomProduct' },
additionalPrice : { type : 'integer', defaultsTo: 0 },
// assets
display : { model : 'CmsProductVariant' },
// store referrer
store : { model : 'SystemStore' }
}
};
CmsProductVariant.js
module.exports = {
schema : true,
attributes: {
// custom product variant reference
product : { model : 'PosProductVariant' },
// custom title
title : { type : 'string' },
gallery : { type : 'array' , defaultsTo : [] }, // main gallery
}
};
PosProductController.js
module.exports = {
updatePricing : function(req,res,next){
// get pricing parameters
var pricing = req.allParams(),
query = {};
// parameters violation
if(!pricing.additionalPrice)
return res.badRequest();
// compose query
for(var attr in pricing){
if(attr !== 'additionalPrice' && _.isArray(pricing[attr])){
query[attr] = pricing[attr];
}
}
sails.log.verbose('pricing parameter');
sails.log.verbose(pricing);
sails.log.verbose('update query');
sails.log.verbose(query);
PosProductVariant.update(query,{
additionalPrice : pricing.additionalPrice,
display : pricing.display.gallery
})
.then(function(_variant){
if(!_variant) throw 'Cant update Variant';
res.ok();
})
.catch(function(error){
sails.log.error(error);
res.serverError(error);
});
}
};
pricing.html
<form class="grid-fluid grid-parent" name="pricingForm" ng-if="isDefined(product)"
ng-disabled="isProcessing" ng-submit="updatePricing()" novalidate>
<div class="form-group grid-50" ng-repeat="(attr, options) in product.attributes">
<label>{{attr}}</label>
<select multiple ng-model="pricing[attr]" class="form-control" ng-options="option for option in options" required></select>
</div>
<div class="form-group grid-100">
<label>add price</label>
<input type="number" class="form-control" ng-model="pricing.additionalPrice" placeholder="add price" required />
</div>
<div class="form-group grid-100">
<carousel class="bg-dark">
<slide ng-repeat="slide in pricing.display.gallery" active="slide.active" index="$index">
<img ng-src="{{slide.gallery}}" style="margin:auto;">
<div class="carousel-caption">
<a class="button button-error button-labeled"
ng-click="removeItem(pricing.display.gallery,$index)">
<span class="button-label"><i class="icon icon-trash-a"></i></span>
delete image
</a>
</div>
</slide>
</carousel>
<hr/>
<div class="form-group">
<label>tambahan galeri</label>
<file-uploader class="form-control"
image-width="{{galleryConfigs.width}}"
image-height="{{galleryConfigs.height}}"
image-crop-width="{{galleryConfigs.width}}"
image-crop-height="{{galleryConfigs.height}}"
max-size="5000"
allowed-extensions="png,jpeg,jpg"
result-model="addGallery">
</file-uploader>
</div>
</div>
<!-- action -->
<div class="form-group grid-100">
<hr/>
<button type="submit" class="button button-primary button-labeled"
ng-disabled="pricingForm.$invalid || pricingForm.$pristine || isProcessing" >
<span class="button-label"><i class="icon icon-paper-airplane"></i></span>
Update Pricing
</button>
<button class="button button-default button-labeled pull-right" role="action"
ng-click="goBack()" ng-disabled="isProcessing">
<span class="button-label"><i class="icon icon-android-close"></i></span>
Back
</button>
</div>
</form>
And controller pricing.js
var store = {},
id = $stateParams.modelId,
filter = {};
// get current active store
store = authService.getCurrentStore($scope);
if(!store){
// session expired
$scope.$emit(GLOBAL_EVENTS.SYSTEM.STORE.SESSION_TIMEOUT);
return;
}
// init scope
$scope.product = {};
$scope.pricing = {
display: {
gallery: []
}
};
moduleConfigResolver.syncStoreConfig(store.id)
.then(function(STORE_CONFIG){
// get gallery image size configs
$scope.galleryConfigs = {
height : [STORE_CONFIG.POS_PRODUCT_ZOOM_HEIGHT,
STORE_CONFIG.POS_PRODUCT_GALLERY_HEIGHT,
STORE_CONFIG.POS_PRODUCT_THUMBNAIL_HEIGHT],
width : [STORE_CONFIG.POS_PRODUCT_ZOOM_WIDTH,
STORE_CONFIG.POS_PRODUCT_GALLERY_WIDTH,
STORE_CONFIG.POS_PRODUCT_THUMBNAIL_WIDTH]
};
});
// add gallery image
$scope.addGallery = function(imageSrc){
if(!angular.isArray(imageSrc) || imageSrc.length < 3)
return;
$scope.pricing.display.gallery.push({
zoom : imageSrc[0],
gallery : imageSrc[1],
thumbnail : imageSrc[2]
});
};
// if has url params
if(id){
dataProvider.CustomProduct.get({'id':id}).$promise
.then(function(_product_customize){
$scope.product = angular.copy(_product_customize);
})
.catch(function(error){
$log.error(error);
});
It looks to me that the form does not submit data gallery, as the parameters are printed as undefined/null in my posproductcontroller and get an error.
{
"error": "E_VALIDATION",
"status": 400,
"summary": "1 attribute is invalid",
"model": "CmsProductVariant",
"invalidAttributes": {
"hashedPassword": [
{
"display": CmsProductVariant,
"message": "`undefined` should be a string (instead of \"null\", which is a object)"
}
]
}
}
what is my problem with my code? Help me please Sir?