I made a menu with custom block in wordpress gutenberg and made it editable using RichText component but every time I try to edit those, changes will apply to all of them, instead I want individual texts.
I know it can be done by looping through objects but I don't know how
Sidebar block:
import { RichText } from "#wordpress/block-editor"
import { registerBlockType } from "#wordpress/blocks"
registerBlockType("ourblocktheme/sidebarmenucontent", {
attributes: {
text: {type: 'string'},
size: {type: 'string'}
},
title: "Sidebar Menu Content",
edit: EditComponent,
save: SaveComponent
})
function EditComponent(props) {
function textHandler(x) {
props.setAttributes({ text: x })
}
return (
<>
<ul className="main-menu">
<li>
<a data-scroll-nav="0" href="#home">
<span className="m-icon">
<i className="bi-house-door"></i>
</span>
<RichText value={props.attributes.text} onChange={textHandler} />
</a>
</li>
<li>
<a data-scroll-nav="1" href="#services">
<span className="m-icon">
<i className="bi-person"></i>
</span>
<RichText value={props.attributes.text} onChange={textHandler} />
</a>
</li>
<li>
<a data-scroll-nav="2" href="#services">
<span className="m-icon">
<i className="bi-briefcase"></i>
</span>
<RichText value={props.attributes.text} onChange={textHandler} />
</a>
</li>
<li>
<a data-scroll-nav="3" href="#work">
<span className="m-icon">
<i className="bi-columns"></i>
</span>
<RichText value={props.attributes.text} onChange={textHandler} />
</a>
</li>
<li>
<a data-scroll-nav="4" href="#contactus">
<span className="m-icon">
<i className="bi-telephone"></i>
</span>
<RichText value={props.attributes.text} onChange={textHandler} />
</a>
</li>
</ul>
</>
)
}
function SaveComponent() {
return (
<div>Hello</div>
)
}
Result:
In your block attributes, an array of strings is needed to enable storing multiple text strings for your menu items. Looking at the structure of <ul><li><a><span><i>..</i></a></li><ul>, an attribute query can extract the values from the markup into a useful array, eg:
attributes: {
menuItems: {
type: 'array',
source: 'query', // Searches the markup
selector: 'li', // for each <li> element
query: {
text: {
type: 'string',
selector: 'span.label', // then find this selector
source: 'text' // get value of text
},
href: {
type: 'string',
source: 'attribute', // and this attributes
selector: 'a[href]' // value of link href
},
className: {
type: 'string',
source: 'attribute', // and this attributes
selector: 'i[class]' // value of class
}
}
}
}
An additional <span className="label">{text}</span> inside <a>...</a> is required to make the query work as expected. If we queried the text of <a> as in the current markup, it would include all the <span><i>...</i></span> content as part of the string (which shouldn't be edited as part of the <RichText> component).
The resulting array structure can then be used as the "default": [] value for the menuItems attribute:
[
{
text: 'Home',
href: '#home',
className: 'bi-house-door'
},
{
text: 'Services',
href: '#services',
className: 'bi-person'
},
... // etc..
]
The next step as anticipated, is to do a "loop" in edit() to change each text value independently. I prefer map() rather than a loop as I find more compact/cleaner and includes a useful index, eg:
edit()
function EditComponent({attributes, setAttributes}) {
const { menuItems } = attributes;
// TODO: Add updateMenuItem function defintion here..
return (
<ul className="main-menu">
{menuItems && menuItems.map(({ text, href, className }, index) => {
// If there are menuItems, map them out
return (
<li key={index}>
<a href={href}>
<span className="m-icon">
<i className={className}></i>
</span>
<RichText
value={text}
tagName="span" // tagName and className needed for query
className="label" onChange={} // TODO: Call updateMenuItem() onChange
/>
</a>
</li>
)
})}
</ul>
);
}
Unfortunately, setAttributes() doesn't work for an array of objects, so a helper function is required to avoid mutations on an existing object, eg:
...
function updateMenuItem(text, href, className, index) {
const updatedMenuItems = [...menuItems]; // Copy of existing menuItems array
updatedMenuItems[index] = { text: text, href: href, className: className }; // Update the targeted index with new values
setAttributes({ menuItems: updatedMenuItems }); // Update menuItems with the new array
}
...
<RichText
...
onChange={(text) => updateMenuItem(text, href, className, index)}
/>
...
The final step is to ensure the content is saved to markup correctly by using the same map technique as in edit() - with a minor change to <RichText.Content> needed:
save()
function SaveComponent({attributes}) {
const { menuItems } = attributes;
return (
<ul>
{menuItems && menuItems.map(({ text, href, className }, index) => {
return (
<li key={index}>
<a data-scroll-nav={index} href={href}>
<span className="m-icon">
<i className={className}></i>
</span>
<RichText.Content tagName="span" className="label" value={text} />
</a>
</li>
)
})}
</ul>
);
}
If you have reached this far, hopefully you have a working, editable example based on your menu. As with everything, there are many ways to do something and while the above "works", I would suggest more block-like approach in general:
If you break your menu down, it is multiple links styled as buttons. The [core/button] blocks is very close to that, as is [core/list], so I would either extend those blocks, restyle them or make your own that is just the "menu item" link/button content. I would create a parent "menu" block and use InnerBlocks to be able to add and edit as many "menu items" as you need, this would eliminate the need for query attribute and looping plus you could easily reorder them.
I find it helpful to reflect on a issue/solution, consider why it works/was challenging and then find an easier way...
Related
im making a posts page and the thing in focus is pagination.
I've created a pagination component that looks like this:
<template>
<nav aria-label="Pagination">
<ul class="pagination justify-content-end">
<li class="page-item" v-if="currentPage !== 1">
<a #click="previous" class="page-link" href="javascript:void(0)" tabindex="-1">Previous</a>
</li>
<li v-for="page in getNumberOfPagesShow"
v-bind:class="{ disabled: page === currentPage }"
class="page-item">
<a #click="clickPage(page)" class="page-link" href="javascript:void(0)">
{{ page }}
</a>
</li>
<li class="page-item" v-if="currentPage !== totalPages">
<a #click="next" class="page-link" href="javascript:void(0)">Next</a>
</li>
</ul>
</nav>
</template>
<script>
export default {
name: "pagination",
props: ['app', 'onClickPage', 'totalPages', 'page'],
data()
{
return {
currentPage: this.page,
lastPage: 0
}
},
computed: {
getNumberOfPagesShow()
{
if (this.totalPages > 10)
{
return 10;
}
return this.totalPages;
}
},
methods: {
previous()
{
this.currentPage--;
this.clickPage(this.currentPage);
},
next()
{
this.currentPage++;
this.clickPage(this.currentPage);
},
clickPage(page)
{
this.currentPage = page;
this.onClickPage(page);
}
}
}
</script>
<style scoped>
</style>
and the component is called using
<!-- Pagination Top -->
<pagination :total-pages="thread.posts.last_page"
:page="app.$route.query.page"
style="margin-top: 20px;"
:on-click-page="clickPage">
</pagination>
Everything works, except the :page="app.$route.query.page" attribute, which sets the currentPage inside the pagination component.
Now, this code doesn't work:
<li class="page-item" v-if="currentPage !== 1">
<a #click="previous" class="page-link" href="javascript:void(0)" tabindex="-1">Previous</a>
</li>
It's supposed to hide the previous button if current page is 1. However, this doesn't work, suggesting that app.$route.query.page is not getting the value correctly.
When I debug inside the created() method, I write
console.log(this.app.$route.query.page)
it does return the correct value. So I don't know what the problem is.
Thank you in advance!
Great, the solution was to parse the prop page into int:
data()
{
return {
currentPage: parseInt(this.page),
lastPage: 0
}
},
Alert: Learning Vue :)
I have a list of items displayed in tables along with status button
I tried to use the v-for but than my search and sortable options doesn't work so I am displaying data using php (laravel) So I only want my status button to work with Vue
I have so far managed to make it work but the only problem is class binding not working for clicked item and it is changing for all of the buttons
here are my html
<td class="center">
<a href="#" class="btn btn-mini">
<i class="fa fa-fw" :class="{'fa-thumbs-o-up': form.isStatus, 'fa-thumbs-o-down': !form.isStatus }" #click="onStatus({{$row->id}})">
</i></a>
</td>
here are my due codes
new Vue({
el: '#viewall',
data: {
form: new Form({
status: '',
isStatus: true
}),
errors: new Errors()
},
methods: {
onStatus(id) {
this.form.statusUpdate('post', 'updatestatus', id)
}
}
})
class Form {
constructor() {}
// update status
statusUpdate(requestType, url, id) {
let data = new FormData()
data.set('pageid', id)
return new Promise((resolve, reject) => {
axios[requestType](url, data)
.then(response => {
this.isStatus = response.data.new_status
})
.catch(error => {})
})
}
}
To answer this:
the only problem is class binding not working for clicked item and it is changing for all of the buttons
Obviously because all of your buttons depends on the same one variable isStatus.
You should have different isStatus for each button, like isStatus_n:
But I understand that you don't have those dynamic Vue data property because it is from Laravel.
So you can do like:
<td class="center">
<a href="#" class="btn btn-mini">
<i class="fa fa-fw" :class="{'fa-thumbs-o-up': form.isStatus_{{$row->id}} && form.isStatus_{{$row->id}}.selected, 'fa-thumbs-o-down': !form.isStatus_{{$row->id}} }" #click="onStatus({{$row->id}})">
</i>
</a>
</td>
Then on your method:
// This will insert/update the form.isStatus_n
var newSet = {selected: true};
this.$set(this.form, 'isStatus_'+id, newSet);
Made a simple demo: https://jsfiddle.net/j9whxnfs/37/
If I have a link:
Click Me
I know I can clickLink based on its text.
public function testCanClickLink()
{
$this->browse(function ($browser) {
$browser->visit('/welcome')
->clickLink('Click Me');
});
}
But how can I click an icon link?
<a href="/somewhere">
<i class="fa fa-plus" aria-hidden="true"></i>
</a>
You can target the href like this:
->click('a[href="/somewhere"]')
This is a bit hacky, but it's what I've come up with as a workaround.
Put an id selector on the link.
<a id="link-click-me" href="/somewhere">
<i class="fa fa-plus" aria-hidden="true"></i>
</a>
Assert it's visible.
Get the href attribute.
Visit it.
Assert path is correct.
public function testCanClickLink()
{
$this->browse(function ($browser) {
$browser->visit('/welcome')
->assertVisible('#link-click-me')
->visit(
$browser->attribute('#link-click-me', 'href')
)
->assertPathIs('/somewhere');
});
}
I am building an application with Codeigniter and Bootstrap.
What I want to do is to have a dropdown menu which can change a status in sql, and then in background via AJAX call PHP script to do that and display a confirmation message - with bootstrap alert. Everything should be done without page refresh.
The problem is that I can't find a solution to pass the variable from drop down to PHP without page refresh via POST.
I am trying to do something like this:
<!-- AJAX which hide the DIV -->
<script>
$(document).ready(function(){
$("#message").hide();
$('#status li').click(function () {
$("#message").show();
$("#success-alert").load('<?php echo base_url() ?>/changestatus/150/', {my_var : $("#my_var").val()});
$("#message").fadeTo(2000, 500).slideUp(500, function(){
$("#message").hide();
});
});
});
</script>
In the above code, I would like to have a link like: /changestatus/150/2
where 150 is a sample lead id, and 2 is a new status choosen from dropdown.
<!-- Alert DIV, which is hidden on load -->
<div id="message">
<div class="alert alert-success" id="success-alert">
<button type="button" class="close" data-dismiss="alert">x</button>
<strong>OK! </strong>
The changes has been done :-)
</div>
</div>
<!-- Dropdown menu -->
<div class="btn-group" id="myDropdown">
<a class="btn dropdown-toggle" data-toggle="dropdown" href="#">
Menu
<span class="caret"></span>
</a>
<ul class="dropdown-menu" id="status">
<li>Chg status to 1</li>
<li>Chg status to 2</li>
<li>Chg status to 3</li>
</ul>
</div>
In the above drop down, I do not know where to put the ID numbers 1,2,3.
and how to send it as my_var to AJAX when user clicks option
..and function in my controller
<?
public function changestatus($lead_id)
{
//read a new status from segment in link...
$new_status = $this->uri->segment(4),
//...or from POST
if(isset($_POST['my_var']))
{
$new_status = $_POST['my_var'];
}
// then will be some call to model which mades changes to DB
echo '...done';
}
?>
I've spent a whole day trying to do without success, please help if you can :)
You can make an extra attribute to all your li and assign them values. Get those data attributes in your jQuery code and pass it to the ajax request page.
<ul class="dropdown-menu" id="status">
<li data="1">Chg status to 1</li>
<li data="2">Chg status to 2</li>
<li data="3">Chg status to 3</li>
</ul>
Change jQuery code as below:
$('#status li').click(function () {
var my_var = $(this).attr('data'); //Use this value for your page
$("#message").show();
$("#success-alert").load('<?php echo base_url() ?>/changestatus/150/', {my_var : my_var});
$("#message").fadeTo(2000, 500).slideUp(500, function(){
$("#message").hide();
});
});
Instead of using $("#success-alert").load() you should use $.post to send post data (enter link description here).
$.load uses GET to fetch data from web server.
I was reading the manual about basic placeholder usage, and it has this example:
class Bootstrap extends Zend_Application_Bootstrap_Bootstrap
{
// ...
protected function _initSidebar()
{
$this->bootstrap('View');
$view = $this->getResource('View');
$view->placeholder('sidebar')
// "prefix" -> markup to emit once before all items in collection
->setPrefix("<div class=\"sidebar\">\n <div class=\"block\">\n")
// "separator" -> markup to emit between items in a collection
->setSeparator("</div>\n <div class=\"block\">\n")
// "postfix" -> markup to emit once after all items in a collection
->setPostfix("</div>\n</div>");
}
// ...
}
I want to accomplish almost exactly that, but I'd like to conditionally add more class values to the repeating divs, at time of rendering if possible, when all the content is in the placeholder. One thing I specifically want to do is add the class of "first" to the first element and "last" to the last element. I assume that I'll have to extend the Zend_View_Helper_Placeholder class to accomplish this.
The string set with setSeparator() is what will be used to implode elements in a container. Either set it to an empty string or just leave out the call to setSeparator() and insert the separating divs along with your other content:
<?php $this->placeholder('sidebar')->captureStart(); ?>
<?php if($userIsAdmin === TRUE) { ?>
<div class="block admin-menu">
<h4>User Administration</h4>
<ul>
<li> ... </li>
<li> ... </li>
</ul>
</div>
<?php } ?>
<div class="block other-stuff">
<h4>Non-Admin Stuff</h4>
<ul>
<li> ... </li>
<li> ... </li>
</ul>
</div>
<?php $this->placeholder('sidebar')->captureEnd() ?>