On connect, I want to add some markup that is connected to an action within the same controller. Is this possible? How is it done?
This is what I am trying so far in my tricksmods controller:
addEmbedButton() {
const buttonHTML = '<button type="button" class="trix-button" data-trix-attribute="embed" data-action="click->tricksmods#showembed" title="Embed" tabindex="-1">Embed</button>'
this.buttonGroupBlockTools.insertAdjacentHTML("beforeend", buttonHTML)
}
showembed(){
console.log('showembed')
}
The markup is added, but the showembed action is not fired on click.
I worked this out. I needed to use the toolbarElement of the trix editor to get at my button and dialog.
import { Controller } from "#hotwired/stimulus"
import Trix from 'trix'
import Rails from "#rails/ujs"
export default class extends Controller {
static get targets() {
return [ "field" ]
}
connect() {
this.addEmbedButton()
this.addEmbedDialog()
this.eventListenerForEmbedButton()
this.eventListenerForAddEmbedButton()
}
addEmbedButton() {
const buttonHTML = '<button type="button" class="trix-button tricks-embed" data-trix-attribute="embed" data-trix-action="embed" data-action="click->tricks#showembed" title="Embed" tabindex="-1">Embed</button>'
this.buttonGroupBlockTools.insertAdjacentHTML("beforeend", buttonHTML)
}
addEmbedDialog() {
const dialogHTML = `<div class="trix-dialog trix-dialog--link" data-trix-dialog="embed" data-trix-dialog-attribute="embed" data-tricks-target="embeddialog">
<div class="trix-dialog__link-fields">
<input type="text" name="embed" class="trix-input trix-input--dialog" placeholder="Paste your URL" aria-label="embed code" required="" data-trix-input="" disabled="disabled">
<div class="trix-button-group">
<input type="button" class="trix-button trix-button--dialog" data-trix-custom="add-embed" value="Add">
</div>
</div>
</div>`
this.dialogsElement.insertAdjacentHTML("beforeend", dialogHTML)
}
showembed(e){
console.log('showembed')
const dialog = this.toolbarElement.querySelector('[data-trix-dialog="embed"]')
const embedInput = this.dialogsElement.querySelector('[name="embed"]')
if (event.target.classList.contains("trix-active")) {
event.target.classList.remove("trix-active");
dialog.classList.remove("trix-active");
delete dialog.dataset.trixActive;
embedInput.setAttribute("disabled", "disabled");
} else {
event.target.classList.add("trix-active");
dialog.classList.add("trix-active");
dialog.dataset.trixActive = "";
embedInput.removeAttribute("disabled");
embedInput.focus();
}
}
eventListenerForEmbedButton() {
this.toolbarElement.querySelector('[data-trix-action="embed"]').addEventListener("click", e => {
this.showembed(e)
})
}
eventListenerForAddEmbedButton() {
this.dialogsElement.querySelector('[data-trix-custom="add-embed"]').addEventListener("click", event => {
console.log('embeddy')
const content = this.dialogsElement.querySelector("[name='embed']").value
if (content) {
let _this = this
let formData = new FormData()
formData.append("content", content)
Rails.ajax({
type: 'PATCH',
url: '/admin/embed.json',
data: formData,
success: ({content, sgid}) => {
const attachment = new Trix.Attachment({content, sgid})
_this.element.editor.insertAttachment(attachment)
_this.element.editor.insertLineBreak()
}
})
}
})
}
//////////////// UTILS ////////////////////////////////////////////////////
get buttonGroupBlockTools() {
return this.toolbarElement.querySelector("[data-trix-button-group=block-tools]")
}
get buttonGroupTextTools() {
return this.toolbarElement.querySelector("[data-trix-button-group=text-tools]")
}
get buttonGroupFileTools(){
return this.toolbarElement.querySelector("[data-trix-button-group=file-tools]")
}
get dialogsElement() {
return this.toolbarElement.querySelector("[data-trix-dialogs]")
}
get toolbarElement() {
return this.element.toolbarElement
}
}
Related
I'm clicking a button to make a call to an API to get a random fact and length. This is then displayed in two input fields which works. I'm struggling with how to change the text in the input fields to the new fact and length from the API when the button is clicked again. I know it is something simple but can't seem to find the solution. Any help would be appreciated.
<template>
<form>
<header>
<img alt="Vue logo" class="logo" src="./assets/logo.svg" width="300" height="300" />
<br>
<br>
<input type="text" id="idFact" v-model="facts">
<br>
<br>
<input type="text" id="idLength" v-model="lengths">
<br>
<br>
<button v-on:click="callAPI" type="button">Call</button>
</header>
</form>
</template>
<script>
export default {
data() {
return {
posts: '',
facts: '{fact}',
lengths: '{length}',
};
},
methods: {
async getData() {
try {
let response = await fetch("https://catfact.ninja/fact");
this.posts = await response.json();;
} catch (error) {
console.log(error);
}
},
callAPI() {
this.facts = this.posts.fact
this.lengths = this.posts.length
}
},
created() {
this.getData();
}
}
</script>
Thanks
Invoke the getData method inside the click handler callAPI, and remove the call from created hook in order to avoid redundant calls:
<script>
export default {
data() {
return {
posts: '',
facts: '{fact}',
lengths: '{length}',
};
},
methods: {
async getData() {
try {
let response = await fetch("https://catfact.ninja/fact");
this.posts = await response.json();;
} catch (error) {
console.log(error);
}
},
callAPI() {
this.getData()
this.facts = this.posts.fact
this.lengths = this.posts.length
}
},
}
</script>
In a recent web app we have a lot of forms with the same submit structure:
Disable the form and submit button based on an isSubmitting variable
Validate the input fields (we're using Yup)
If validation fails: Set isSubmitting back to false + set and show validationErrors on the input fields
If validation succeed: Send post request with form data to api
Show general error if api is down or returns an error
I've tried to something using the composition api in vue 3.
Login.vue
<template>
<div class="min-h-full flex flex-col justify-center py-12 sm:px-6 lg:px-8">
<div class="sm:mx-auto sm:w-full sm:max-w-md">
<h1 class="text-3xl text-center text-gray-900">{{ t('sign_in_account', 1) }}</h1>
</div>
<div class="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
<div class="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10">
<form #submit.prevent="handleSubmit">
<fieldset :disabled="isSubmitting" class="space-y-6">
<MessageBox v-if="errors.general" :title="errors.general" :messages="errors.messages" />
<Input :label="t('email', 1)" type="text" id="email" v-model="user.email" :error="errors.email" />
<Password :label="t('password', 1)" type="password" id="password" v-model="user.password" :error="errors.password" />
<div class="text-sm text-right">
<router-link class="font-medium text-indigo-600 hover:text-indigo-500" :to="forgotPassword">{{ t('forgot_password', 1) }}</router-link>
</div>
<SubmitButton class="w-full" :label="t('sign_in', 1)" :submittingLabel="t('sign_in_loader', 1)" :isSubmitting="isSubmitting" />
</fieldset>
</form>
</div>
</div>
</div>
</template>
<script>
import { ref } from 'vue';
import { useStore } from 'vuex';
import { useI18n } from 'vue-i18n';
import useForm from '#/use/useForm';
import { validateEmail, LoginValidationSchema } from '#/utils/validators';
export default {
setup() {
const store = useStore();
const { t } = useI18n({ useScope: 'global' });
const user = ref({
email: '',
password: '',
});
const { handleSubmit, isSubmitting, errors } = useForm(user, LoginValidationSchema, handleLogin);
async function handleLogin(values) {
try {
return await store.dispatch('auth/login', values);
} catch (error) {
if (error.response) {
console.log(error.reponse);
if (error.response.status == 422) {
errors.value = {
general: `${t('unable_to_login', 1)}<br /> ${t('fix_and_retry', 1)}`,
messages: Object.values(error.response.data.errors).flat(),
};
} else if (error.response.data.message) {
errors.value = {
general: error.response.data.message,
};
} else {
errors.value = {
general: `${t('unknown_error', 1)}<br /> ${t('please_try_agin', 1)}`,
};
}
} else if (error.request) {
console.log(error.request);
errors.value = {
general: `${t('unknown_error', 1)}<br /> ${t('please_try_agin', 1)}`,
};
} else {
errors.value = {
general: `${t('unknown_error', 1)}<br /> ${t('please_try_agin', 1)}`,
};
}
return;
}
}
return { t, user, handleSubmit, isSubmitting, errors };
},
computed: {
forgotPassword() {
return validateEmail(this.user.email) ? { name: 'forgotPassword', query: { email: this.user.email } } : { name: 'forgotPassword' };
},
},
};
</script>
useForm.js
import { ref, watch } from 'vue';
export default function useForm(initialValues, validationSchema, callback) {
let values = ref(initialValues);
let isSubmitting = ref(false);
let errors = ref({});
async function handleSubmit() {
try {
errors.value = {};
await validationSchema.validate(values.value, { abortEarly: false });
isSubmitting.value = true;
} catch (err) {
console.log('In the catch');
isSubmitting.value = false;
err.inner.forEach((error) => {
errors.value = { ...errors.value, [error.path]: error.message };
});
}
}
watch(isSubmitting, () => {
if (Object.keys(errors.value).length === 0 && isSubmitting.value) {
callback(values);
isSubmitting.value = false;
} else {
isSubmitting.value = false;
}
});
return { handleSubmit, isSubmitting, errors };
}
This is somehow working but I'm missing two things. In useForm I want to wait till the callback is done (succeed or failed) to set isSubmitting back to false. Is a promise a good way to do this of is there a better way? Secondly I want a reusable way to handle the errors in Login.vue. Any suggestion how to handle this?
Regarding your first question - try..catch statements have a third statement called finally which always executes after the try statement block has completed.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/try...catch
To answer your second question - promises are a great way of handling async logic, including your case when the API you're sending the request to returns an error response, and you can then decide how you're going to handle the UX in such scenario.
I'm not quite clear on what you mean by handle the errors in Login.vue in a reusable way, but perhaps you could simple pass in an empty array prop to your useForm called formErrors and have your useForm.js emit an update:modelValue event to get two way binding.
I just started learning Vue.js. I need to call the function in another component in my project. When I add new data to the table with createAnnouncement.vue, I want to go into announcement.vue and call the queryAnnouncement function. How can I do that? I would appreciate if you could help, please explain with a sample. Or edit my codes.
Announcement.Vue template:
<template>
// more div or not important template code
<div class="dataTables_filter" style="margin-bottom:10px">
<label>
<input class="form-control form-control-sm" placeholder="Search" aria-controls="m_table_1" type="search" v-model="searchField" #change="filter()">
</label>
<a style="float:right" href="#" data-target="#create-announcement-modal" data-toggle="modal" class="btn btn-primary">
<i class="">Add</i>
</a>
</div>
// more div or not important template code
</template>
Announcement.Vue Script Code:
<script>
import toastr from "toastr";
export default {
name: 'announcement',
data() {
return {
announcements: [],
searchField: "",
deleteCsrfToken: this.$root.csrfTokens["deleteAnnouncement"]
}
},
beforeMount: async function () {
await this.queryAnnouncements();
},
methods: {
filter: async function () {
await this.queryAnnouncements(this.searchField);
},
queryAnnouncements: async function (filter, pageSize, pageIndex, sortBy, sortType) {
var data = {
"query[general-filter]": filter,
"pagination[perpage]": !!pageSize ? pageSize : 10,
"pagination[page]": !!pageIndex ? pageIndex : 1,
"sort[field]": sortType,
"sort[sort]": !!sortBy ? sortBy : "asc"
};
let response = await axios
.get("/Announcement/QueryAnnouncements", { params: data })
this.announcements = response.data.data;
},
}
}
createAnnouncement.vue code:
<template>
<button #click="createAnnouncement()" v-bind:disabled="contentDetail === ''" class="btn btn-success">
Save</button>
//not important template codes
<template>
<script>
import toastr from "toastr";
export default {
name: 'create-announcement',
data() {
return {
contentDetail: "",
createCsrfToken: this.$root.csrfTokens["createAnnouncement"],
}
},
methods: {
createAnnouncement: async function () {
var self = this;
var data = {
content: this.contentDetail,
};
let response = await axios
.post("/Announcement/createAnnouncement",
data,
{
headers: {
RequestVerificationToken: self.createCsrfToken
}
})
if (response.status == 200) {
$("#create-announcement-modal .close").click()
$("#create-announcement-form").trigger('reset');
toastr["success"]("Kayıt başarıyla eklendi!", "Başarılı!");
self.contentDetail = "";
}
else {
toastr["warning"]("Hata", "Kayıt Eklenemedi.");
}
}
},
}
</script>
Please show with sample or arrangement, my english is not very good. Thanks.
I want to know the best way I can give user inline edit in ag-grid on button click.
see the image below.
As per my requirement, if user clicks on edit icon, then ag-grid row goes in fullrow edit mode (able to do from documentation provided onag-grid.com) and at the same time, icons in 'Action' column changes to save and cancel icons. So, want to know how this can be done in Angular5. I need idea of dynamically changing this last column.
There's quite a few steps here that you'll need to implement.
Step 1: Create a custom renderer component
#Component({
selector: 'some-selector',
template: `
<span *ngIf="!this.isEditing">
<button (click)="doEdit()">Edit</button>
</span>
<span *ngIf=this.isEditing">
<button (click)="doSave()">Save</button>
<button (click)="doCancel()">Cancel</button>
</span>
`
})
export class MyRenderer implements ICellRendererAngularComp {
isEditing = false;
params: any;
agInit(params: any): void {
this.params = params;
}
doEdit() {
// we need to loop thru all rows, find the column with the renderer, and 'cancel the edit mode'.
// otherwise, we will end up with rows that has SAVE buttons appearing but not in edit mode
const renderersInOtherRows = this.params.api.getCellRendererInstances(this.params);
if( renderersInOtherRows && renderersInOtherRows.length > 0 ) {
for ( let i=0; i<renderersInOtherRows.length; i++) {
const wrapper = renderersInOtherRows[i];
if ( wrapper.getFrameworkComponentInstance() instanceof MyRenderer ) {
const foundRenderer = wrapper.getFrameworkComponentInstance() as MyRenderer;
if( foundRenderer.isEditing ) {
foundRenderer.doCancel();
}
}
}
}
this.isEditing = true;
this.params.api.startEditingCell( { rowIndex: this.params.node.rowIndex, colKey: 'some-col-field-name'});
}
doCancel() {
this.isEditing = false;
// restore previous data or reload
}
doSave() {
this.isEditing = false;
// do your saving logic
}
}
Step 2: Load the component
#NgModule({
imports:[
AgGridModule.withComponents([MyRenderer]),
// ...
],
declarations: [MyRenderer],
})
export class MyModule();
Step 3: Use the component
SuppressClickEdit = true, will prevent double/single click edit mode
#Component({
selector: 'my-grid',
template: `
<ag-grid-angular #grid
style="width: 100%; height: 500px;"
class="ag-theme-balham"
[rowData]="this.rowData"
[columnDefs]="this.columnDefs"
[editType]="'fullRow'"
[suppressClickEdit]="true"></ag-grid-angular>
`
})
export class MyGridComponent implements OnInit {
columnDefs = [
// ... other cols
{ headerName: '', cellRendererFramework: MyRenderer }
];
}
I was just looking for something similiar and so I thought I would share what I did to get this working. I am new to Angular so this may not be the best way.
This is in my component.html
<button (click)="onEdit()">edit button</button>
<button (click)="onSaveEdit()" *ngIf="!editClicked">save button</button>
<button (click)="onCancelEdit()" *ngIf="!editClicked">cancel</button>
This is in my component.ts
public params: any;
private editClicked;
constructor() {
this.editClicked = true;
}
agInit(params: any): void{
this.params = params;
}
onEdit() {
this.editClicked = !this.editClicked;
this.params.api.setFocusedCell(this.params.node.rowIndex, 'action');
this.params.api.startEditingCell({
rowIndex: this.params.node.rowIndex,
colKey: 'action'
});
}
onSaveEdit() {
this.params.api.stopEditing();
this.editClicked = !this.editClicked;
}
onCancelEdit() {
this.params.api.stopEditing(true);
this.editClicked = !this.editClicked;
}
Hope this helps steer you in the right direction.
I tried to find a way for having and manage an angular2 Component in a Service but with no success:
I need to create:
AlertService{
alertConfirm(msg): Promise;
}
alertConfirm will prompt an Confirmation window with 2 buttons (Ok, Cancel) and will return users' choise as a Promise.
In General, the idea is to implement the famous JavaScript alert() method
but with a designed UI window and with also a cancel button.
The method will return a Promise with a response of user's choice: "OK" or "Cancel".
I tried to find a way for holding an "anonymous" component, AlertComponent, in AlertService:
AlertComponent{
showMsgConfirm(msg): Promise;
}
The Promise will be set with a response when user close prompt window or click "OK" or "Cancel".
The question:
How to make "AlertService" to have an inner "AlertComponent" which can be managed by it's "alertOK" method?
I mean, I didn't find a way for "alertConfirm" to call "showMsgConfirm" method and to return it's Promise as a response.
for example, calling from main app component:
this.alertService.alertConfirm("Save changes?").then(res => {
if(res.ok){console.log("Can be saved");
}, err=> { });
Any ideas for this?
Thanks,
Update:2 different ideas for solution, but with no sucess to manage the AlertComponent:
import { Injectable, ViewContainerRef, ReflectiveInjector, ComponentFactoryResolver, ComponentRef } from '#angular/core';
import { AlertComponent } from './../components/modales/AlertComponent/AlertComponent.component';
#Injectable()
export class AlertService {
constructor(private componentFactoryResolver: ComponentFactoryResolver) { }
public createAlertComp(vCref: ViewContainerRef): ComponentRef<any> {
let factory = this.componentFactoryResolver.resolveComponentFactory(AlertComponent);
/*
//Option 1:
// vCref is needed cause of that injector..
let injector = ReflectiveInjector.fromResolvedProviders([], vCref.parentInjector);
// create component without adding it directly to the DOM
let comp = factory.create(injector);
// add inputs first !! otherwise component/template crashes ..
comp.instance.model = modelInput;
// all inputs set? add it to the DOM ..
vCref.insert(comp.hostView);
return comp;
*/
//Option 2:
var componentRef: ComponentRef<AlertComponent> = vCref.createComponent(factory);
return null;
}
}
And the answer is... :
The Service:
_counter is used for each modal to have a unique name.
comp.instance.close is a property of inner component for subscribing for EventEmitter.
.
import { Injectable, ViewContainerRef, ReflectiveInjector, ComponentFactoryResolver, ComponentRef, EventEmitter } from '#angular/core';
import { CtmAlertComponent } from './ctmAlert/ctmAlert.component';
#Injectable()
export class AlertCtmService {
private _vcr: ViewContainerRef;
private _counter: number = 0;
constructor(private componentFactoryResolver: ComponentFactoryResolver, public viewRef: ViewContainerRef) {
console.log("AlertCtmService.constructor:");
//TODO: Consider appending to this.viewRef: "#alertCtmServiceContainer" as a Dom elemnt perent container which will hold all AlertModals:
// Maybe by:
// this.viewRef.element.nativeElement.insertAdjacentHTML('beforeend', '<div class="alertCtmServiceContainer"></div>');
this._vcr = this.viewRef;
}
public alertOK(alertMsg: string): EventEmitter<any> {
return this.createEventEmitterComponent("CtmAlertComponent", alertMsg, false);
}
public alertConfirm(alertMsg: string): EventEmitter<any> {
return this.createEventEmitterComponent("CtmAlertComponent", alertMsg, true);
}
private createEventEmitterComponent(componentName: string, alertMsg: string, isConfirm: boolean): EventEmitter<any> {
console.log("AlertCtmService.createEventEmitterComponent:");
switch (componentName) {
case "CtmAlertComponent":
default:
var _component = CtmAlertComponent;
break;
}
let factory = this.componentFactoryResolver.resolveComponentFactory(_component);
// vCref is needed cause of that injector..
let injector = ReflectiveInjector.fromResolvedProviders([], this._vcr.parentInjector);
// create component without adding it directly to the DOM
let comp = factory.create(injector);
// add inputs first !! otherwise component/template crashes ..
comp.instance.close.subscribe(resp => {
console.log("AlertCtmService.createEventEmitterComponent: comp.instance.close.subscribe: resp=" + resp.ok);
comp.destroy();
})
comp.instance.alertBodyMsg = alertMsg;
comp.instance.isConfirm = isConfirm;
comp.instance.nameId = "Modal" +(++this._counter).toString();
// all inputs set? add it to the DOM ..
this._vcr.insert(comp.hostView);
//return null;
return comp.instance.close;
}
public init(vCref: ViewContainerRef): ViewContainerRef {
this._vcr = vCref;
return this._vcr;
}
}
Inner Component:
Using Bootstrap for handling display of Modal in UI: modal('show') \ modal('hide').
.
import { Component, AfterViewInit, Input, ViewChild, ElementRef, Renderer, NgZone, EventEmitter} from '#angular/core';
#Component({
selector: 'ctm-alert',
styles: [``],
templateUrl: '/app/shared/alertCtm/ctmAlert/CtmAlert.component.html',
styleUrls: ['./app/shared/alertCtm/ctmAlert/CtmAlert.component.css'],
providers: []
})
export class CtmAlertComponent implements AfterViewInit {
public ModalIsVisible: boolean;
//private static subscriptions: Object = {};
//enums = Enums;
close = new EventEmitter();
public nameId = "";
private isOk = false;
alertBodyMsg: string = "";
isConfirm = false;
constructor() {
console.log("CtmAlertComponent.constructor:");
}
ngAfterViewInit() {
this.showModal();
var attrId = this.getIdAttr();
$('#' + attrId).on('hidden.bs.modal', function () {
debugger;
console.log('CtmAlertComponent: #licenseModal_XXX.on(hidden.bs.modal)');
this.submitStatus();
}.bind(this) );
}
showModal() {
this.ModalIsVisible = true;
var attrId = '#' +this.getIdAttr();
$(attrId).modal('show');
}
hideModal() {
this.ModalIsVisible = false;
var attrId = '#' + this.getIdAttr();
$(attrId).modal('hide');
}
getIdAttr(): string {
return "ctmAlertModal_" + this.nameId;
}
submitStatus() {
var resp = { ok: (this.isOk == true) };
this.close.emit(resp);
}
submitOk() {
this.isOk = true;
this.hideModal();
}
submitCancel() {
this.isOk = false;
this.hideModal();
}
}
App's Declaration:
unfortunately, we must declare the anonymus component in our main-app module.
We must add a declaration of entryComponents: [CtmAlertComponent],
.
import { CtmAlertComponent } from './shared/alertCtm/ctmAlert/ctmAlert.component';
#NgModule({
imports: [
BrowserModule,
HttpModule,
AppRoutingModule,
...
],
declarations: [
CtmAlertComponent,
AppComponent,
...
],
entryComponents: [CtmAlertComponent],
providers: [
...
],
bootstrap: [AppComponent],
})
export class AppModule { }
enableProdMode();
Modal UI:
this html template is based on bootstrap's UI:
.
<div class="ctmAlertModal modal fade in" [id]="getIdAttr()" role="dialog">
<div class="modal-dialog modal-lg" [ngClass]="{'modal-lg-6': true }">
<!-- Modal content-->
<div class="modal-content">
<div class="modal-header" style="">
<div class="pull-right" style="position: relative;">
<span class="fa fa-times-circle" aria-hidden="true" style="color: #949494"></span>
</div>
</div>
<div class="modal-body">
<div class="modal-body-msg">
{{alertBodyMsg}}
</div>
<div class="modal-body-buttons">
<div style="margin: 0 auto;" [style.width]="(isConfirm)? '165px' : '70px' ">
<button type="button" *ngIf="isConfirm" class="btn-submit pull-left btn-cancel" [ngClass]="{'disabled': false }" [disabled]="false" (click)="submitCancel()">
<!--<img alt="End-Training" class="centering-me2" src="../../../contents/training_state_stop_white.svg">-->
Cancel
</button>
<button type="button" class="btn-submit pull-right" [ngClass]="{'disabled': false }" [disabled]="false" (click)="submitOk()">
<!--<img alt="Resume-Training" src="../../../contents/training_state_play_white.svg">-->
OK
</button>
</div>
</div>
</div>
</div>
</div>
</div>
.
Usage::
for example:
.
this.alertCtmService.alertOK("Save changes???").subscribe(function (resp) {
console.log("alertCtmService.alertOK.subscribe: resp=" + resp.ok);
this.saveData();
}.bind(this) );
**
An example I built : https://plnkr.co/qc1ZM6
**
sources:
building-angular-2-components-on-the-fly-a-dialog-box-example
angular2-ngmodule