I'm trying to add the BlockVariationPicker like in the WordPress Github example:
import { useSelect } from '#wordpress/data';
import {
__experimentalBlockVariationPicker as BlockVariationPicker,
store as blockEditorStore,
} from '#wordpress/block-editor';
const MyBlockVariationPicker = ( { blockName } ) => {
const variations = useSelect(
( select ) => {
const { getBlockVariations } = select( blocksStore );
return getBlockVariations( blockName, 'block' );
},
[ blockName ]
);
return <BlockVariationPicker variations={ variations } />;
};
In my edit function I'm adding:
{ MyBlockVariationPicker }
The block variation picker does not show.
I have already registered my bloc variations with scope block:
registerBlockVariation(
'my/testimonial',
[
{
name: 'testimonial-1',
title: 'Testimonial 1',
scope: ['block'],
attributes: {
example: 'testimonial-1'
},
},
{
name: 'testimonial-2',
title: 'Testimonial 2',
scope: ['block'],
attributes: {
example: 'testimonial-2'
},
}
]
);
The block variations should show in { MyBlockVariationPicker } but the don't. Unfortunately there isn't much documentation about this. How can we render the variations of a block using the Block Variation Picker as shown in the Github example?
Both the Columns and Query block use __experimentalBlockVariationPicker and its a really nice component/UI and I agree, it there aren't many examples of how to use it, most likely as its still 'experimental' and still likely to change.
I found that both the Columns and Query blocks display the BlockVariationPicker by checking if the current block (by clientId) contains any InnerBlocks; if there are none, the BlockVariationPicker is shown. When using this component in your own block, you will need some attribute or property to check whether or not a variation has been selected.
I've put together a basic/working example using the structure of your my/testimonial block + variations and based on how the BlockVariationPicker is implemented in Columns block:
import { get } from 'lodash';
import { useSelect } from '#wordpress/data';
import { registerBlockType, registerBlockVariation, store as blocksStore } from '#wordpress/blocks';
import { useBlockProps, __experimentalBlockVariationPicker as BlockVariationPicker } from '#wordpress/block-editor';
// Create our own BlockVariationPicker
const MyBlockVariationPicker = ({ name, setAttributes }) => { // Note: We need "name" and "setAttributes" from edit() props
const { blockType, variations, defaultVariation } = useSelect(
(select) => {
const { getBlockVariations, getBlockType, getDefaultBlockVariation } = select(blocksStore);
return {
blockType: getBlockType(name),
defaultVariation: getDefaultBlockVariation(name, 'block'),
variations: getBlockVariations(name, 'block')
};
},
[name]
);
return <BlockVariationPicker
variations={variations}
icon={get(blockType, ['icon', 'src'])}
label={get(blockType, ['title'])}
onSelect={(nextVariation = defaultVariation) => {
if (nextVariation.attributes) {
setAttributes(nextVariation.attributes); // Use setAttributes to set the selected variation attributes
}
}}
/>;
};
// Register the Block Variations
registerBlockVariation(
'my/testimonial',
[
{
name: 'testimonial-1',
title: 'Testimonial 1',
icon: 'admin-comments', // Added icon so the variation is visibly different (optional)
scope: ['block'],
attributes: {
example: 'testimonial-1'
},
isDefault: true
},
{
name: 'testimonial-2',
title: 'Testimonial 2',
icon: 'admin-links',
scope: ['block'],
attributes: {
example: 'testimonial-2'
},
}
]
);
registerBlockType('my/testimonial', {
title: 'My Testimonial',
keywords: ['testimonial'],
icon: 'admin-post',
attributes: {
example: {
type: "string", // no default set, example is "undefined"
}
},
edit(props) {
const { attributes, setAttributes } = props;
// If example is undefined, show Variation Picker
if (attributes.example === undefined) {
return (
<MyBlockVariationPicker {...props} />
);
}
// Otherwise show the Editor
return (<div {...useBlockProps()}><h2>{attributes.example}</h2></div>);
},
save: ({ attributes }) => {
return <div {...useBlockProps.save()}><h2>{attributes.example}</h2></div>;
}
})
If you build the above javascript, the resulting block allows you to pick from the two variations on insertion:
Related
Been having trouble getting this 'Encountered two children with the same key' error sorted. I've gone through a few threads regarding this issue and none of the answers seem to work in my situation. I want to avoid using index as well as its discouraged.
useEffect(() => {
const Items = locations.map((location) => {
return {
header: location.description,
key: location.code,
content: location.phoneNumber,
image: {
as: Avatar,
},
code: location.code,
};
});
useEffect(() => {
const Items = locations.map((location) => {
return {
header: location.description,
key: location.code,
content: location.phoneNumber,
image: {
as: Avatar,
},
code: location.code,
};
});
The key location.code is not unique and is used multiple times, you need unique keys. A fix could be this :
useEffect(() => {
const Items = locations.map((location,index) => {
return {
header: location.description,
key: index,
content: location.phoneNumber,
image: {
as: Avatar,
},
code: location.code,
};
});
But the best would be something like a unique id. Maybe location.id if thats available..
Is there a unique id in the location object? in which case, that is the perfect solution. Using the index is discouraged, but could potentially be combined with your location.code and concatenated.
useEffect(() => {
const Items = locations.map((location,index) => {
return {
header: location.description,
key: index + location.code,
content: location.phoneNumber,
image: {
as: Avatar,
},
code: location.code,
};
});
I'm working on trying to extend this markdown editor library - which itself extends Prosemirror. I'd like to build my own Node which has the following schema:
attrs: {
id: number
}
and returns the following HTML
<div class=`story-block ${attrs.id}`>
<div contenteditable="false">
<button>Story</button>
</div>
<div>
<p>Text that we can edit</p>
</div>
</div>
The trick here is that since the editor saves to markdown. We need some way to understand this is a special type of node, a Story, but wrapping it in a unique set of characters, I'm using %%%. This is similar to how the library I'm extending using ::: as a signal for their Notice node
I am able to add a new Story node, and it renders my buttons and styles correctly. When I go to save everything saves correctly and I get markdown that looks like
%%%123 // the id
Whatever text I write
%%%
However, when I want to render than markdown back into DOM elements it fails and I get plain text in the document again. Specifically, the DOM looks like
<p>%%%123
Whatever text I write</p>
<p>%%%</p>
Code for the customer node is below. Has anyone successfully extended this library or can show me what I'm doing wrong? Thanks
import { wrappingInputRule } from "prosemirror-inputrules";
import toggleWrap from "../commands/toggleWrap"; //Rule file from library
import Node from "./Node";
export default class Story extends Node {
get name() {
return "container_story";
}
get schema() {
return {
attrs: {
id: {
default: "story",
},
},
content: "block+",
group: "block",
defining: true,
draggable: true,
parseDOM: [
{
tag: "div.story-block",
preserveWhitespace: "full",
contentElement: "div:last-child",
getAttrs: () => ({
id: "story",
}),
},
],
toDOM: node => {
node.attrs.id =
node.attrs.id === "story"
? Math.round(Math.random() * 10000)
: node.attrs.id;
console.log(node.attrs.id);
const button = document.createElement("button");
button.innerText = "Story";
button.id = node.attrs.id;
button.addEventListener(
"click",
(function () {
return function (e) {
alert(`Story ${e.target.id} clicked!`);
};
})(),
false
);
return [
"div",
{ class: `story-block ${node.attrs.id}` },
["div", { contentEditable: false }, button],
["div", { class: "content story-content" }, 0],
];
},
};
}
commands({ type }) {
return attrs => toggleWrap(type, attrs);
}
inputRules({ type }) {
return [wrappingInputRule(/^%%%$/, type)];
}
toMarkdown(state, node) {
state.write("\n%%%" + "story" + "\n");
state.renderContent(node);
state.ensureNewLine();
state.write("%%%");
state.closeBlock(node);
console.log(state);
}
parseMarkdown() {
return {
block: "container_story",
getAttrs: tok => {
console.log(tok.attrGet("id"));
({ id: tok.attrGet("id") });
},
};
}
}
It looks like you are missing a configuration for the markdown rules.
You can add a file in /src/lib/markdown, something like story.ts, with this content (copy/paste from notice.ts)
import customFence from "markdown-it-container";
export default function story(md): void {
return customFence(md, "story", {
marker: "%",
validate: () => true,
});
}
And in the rules.ts file (same dir), you use the new rule
import markdownit from "markdown-it";
...
import storyPlugin from "./story";
export default function rules({ embeds }) {
return markdownit("default", {
breaks: false,
html: false,
})
...
.use(noticesPlugin)
.use(storyPlugin);
}
We've built an app with Vue.js and Firebase where jobs is a node in our db. When we add an event, we want to include the jobs available for that event. On our "Add Event" layout, I've included checkboxes of all the jobs in the DB. We want to be able to select the relevant jobs.
The way it is set up, it works great if you only check one job, but we often need to include 5-10 jobs.
How can I change the code so that I can select multiple jobs at once?
<v-list-tile v-for="(job, index) in jobs" :key="job.title">
<v-list-tile-content>
<v-checkbox
:key="job.title"
:label="job.title"
:value="job.title"
v-model="checkboxes[index].checked">
</v-checkbox>
</v-list-tile-content>
</v-list-tile>
...
export default {
data () {
return {
title: '',
description: '',
startDate: '',
endDate: '',
checkboxes: [],
jobs:[],
...
computed: {
items () {
this.checkboxes = this.jobs.map(job => {
return {
checked:false
}
})
return this.jobs
},
jobs () {
return this.jobs
}
},
... vuex:
created () {
this.jobs = this.$store.getters.loadedJobs
},
Use the index from v-for to keep track of which boxes are checked. Something like the following should get you started:
<v-list-tile v-for="(job, index) in items" :key="job.title">
<v-list-tile-content>
<v-checkbox :value="job.title"
:key="job.title"
:label="job.title"
v-model="checkboxes[index].checked">
</v-checkbox>
</v-list-tile-content>
</v-list-tile>
export default {
data () {
return {
checkboxes: [],
jobs: []
}
},
computed: {
items () {
this.checkboxes = this.jobs.map(job => {
return {
checked: false
}
})
return this.jobs
}
},
created() {
this.$nextTick(() => {
this.jobs = [
{
L9cWVNxnQMfumDkUxxp: {
title: "job 1"
}
},
{
L9cWVNxnQMfumDkUxp: {
title: "job 2"
}
},
{
L9cWVNxnQMfumDkxxp: {
title: "job 3"
}
}]
})
}
}
Working fiddle:
https://jsfiddle.net/6yarqagf/5/
I have this simple query which works fine in my Graphql but I cannot pass data using relay to components and I don't know why :(
{
todolist { // todolist returns array of objects of todo
id
text
done
}
}
this is my code in an attempt to pass data in components using relay:
class TodoList extends React.Component {
render() {
return <ul>
{this.props.todos.todolist.map((todo) => {
<Todo todo={todo} />
})}
</ul>;
}
}
export default Relay.createContainer(TodoList, {
fragments: {
todos: () => Relay.QL`
fragment on Query {
todolist {
id
text
done
}
}
`,
},
});
And lastly my schema
const Todo = new GraphQLObjectType({
name: 'Todo',
description: 'This contains list of todos which belong to its\' (Persons)users',
fields: () => {
return {
id: {
type: GraphQLInt,
resolve: (todo) => {
return todo.id;
}
},
text: {
type: GraphQLString,
resolve: (todo) => {
return todo.text;
}
},
done: {
type: GraphQLBoolean,
resolve: (todo) => {
return todo.done;
}
},
}
}
});
const Query = new GraphQLObjectType({
name: 'Query',
description: 'This is the root query',
fields: () => {
return {
todolist: {
type: new GraphQLList(Todo),
resolve: (root, args) => {
return Conn.models.todo.findAll({ where: args})
}
}
}
}
});
This code looks simple and I cannot see why this won't work and I have this error Uncaught TypeError: Cannot read property 'todolist' of undefined, but I configure todolist and I can query in my graphql, you can see the structure of the query is same, I don't know why this is not working?
todolist should be a connection type on Query. Also, your ids should be Relay global IDs. You will not have access to your objects' raw native id fields in Relay.
import {
connectionArgs,
connectionDefinitions,
globalIdField,
} from 'graphql-relay';
// I'm renaming Todo to TodoType
const TodoType = new GraphQLObjectType({
...,
fields: {
id: uidGlobalIdField('Todo'),
...
},
});
const {
connectionType: TodoConnection,
} = connectionDefinitions({ name: 'Todo', nodeType: TodoType });
// Also renaming Query to QueryType
const QueryType = new GraphQLObjectType({
...,
fields: {
id: globalIdField('Query', $queryId), // hard-code queryId if you only have one Query concept (Facebook thinks of this top level field as being a user, so the $queryId would be the user id in their world)
todos: { // Better than todoList; generally if it's plural in Relay it's assumed to be a connection or list
type: TodoConnection,
args: connectionArgs,
},
},
});
// Now, to be able to query off of QueryType
const viewerDefaultField = {
query: { // Normally this is called `viewer`, but `query` is ok (I think)
query: QueryType,
resolve: () => ({}),
description: 'The entry point into the graph',
}
};
export { viewerDefaultField };
The above is not fully complete (you'll likely also need to setup a node interface on one or more of your types, which will require node definitions), but it should answer your basic question and get you started.
It's a huge, huge pain to learn, but once you struggle through it it starts to make sense and you'll begin to love it over RESTful calls.
I am having a hard time figuring out how to do mutations on plain array via relay.
I am trying to add a new tag to a post.
It does not get updated on client-side after being successfully added on the server-side.
I have to manually reload to see the new tag.
I have tried both REQUIRED_CHILDREN and this.props.relay.forceFetch(), but to no avail.
Also, tried FIELDS_CHANGE for post.
GraphQL Schema:
Post {
id: ID!
text: String!
tags: [Tag!]!
}
Tag {
id: ID!
name: String!
}
AddTagToPostMutation:
static fragments = {
post: () => Relay.QL`
fragment on Post {
id
tags
}
`,
}
getMutation() {
return Relay.QL`mutation { addTagToPost }`;
}
getVariables() {
return {
name: this.props.tag.name,
};
}
getFatQuery() {
return Relay.QL`
fragment on AddTagToPostMutationPayload {
tag {
id
name
}
post {
id
tags
}
}
`;
}
getConfigs() {
return [{
type: 'REQUIRED_CHILDREN',
children: [Relay.QL`
fragment on AddTagToPostMutationPayload {
tag {
id
name
}
post {
id
tags
}
}
`],
}];
}
getOptimisticResponse() {
return {
tag: {
name: this.props.tag.name,
},
post: {
id: this.props.post.id,
},
};
}
As freiksenet already pointed out, FIELDS_CHANGE should be used in getConfigs() function. I took your schema, implemented the GraphQL types, server-side and client-side mutation to add tag to a post. The client-side gets updated successfully. I'm going to just elaborate the solution in my answer.
First, check your server-side mutation. My implementation uses graphql and graphql-relay libraries and looks like below. Notice that the output of the server-side mutation is a post to which a tag has been added. This post is the one whose ID was provided as input.
const AddTagToPostMutation = mutationWithClientMutationId({
name: 'AddTagToPost',
inputFields: {
postId: { type: new GraphQLNonNull(GraphQLID) },
name: { type: new GraphQLNonNull(GraphQLString) },
},
outputFields: {
post: {
type: PostType,
resolve: ({id}) => getPost(id),
},
},
mutateAndGetPayload: ({postId, name}) => {
const id = fromGlobalId(postId).id;
addTagToPost(id, name);
return {id};
},
});
Using graphiql, you can test your mutation:
mutation {
addTagToPost(input:{
postId: "UG9zdDpwb3N0Mg=="
name:"a new tag name"
clientMutationId:"123244"}) {
post {
id
text
tags {
id
name
}
}
}
}
I added a field posts for all posts to the root query. Using graphiql, I first checked the post IDs and used one above.
Using react-relay, the client-side mutation code looks like below. It is passed a prop post whose ID is used as input variable in getVariables() function. In the getConfigs() function, we specify that post field has to be updated. The association between the payload field post and the passed prop post is established using FIELDS_CHANGE mutation type.
export default class AddTagToPostMutation extends Relay.Mutation {
getMutation() {
return Relay.QL`mutation{addTagToPost}`;
}
getVariables() {
return {
postId: this.props.post.id,
name: this.props.name,
};
}
getFatQuery() {
return Relay.QL`
fragment on AddTagToPostPayload {
post {
id,
tags {
id,
name,
}
}
}
`;
}
getConfigs() {
return [{
type: 'FIELDS_CHANGE',
fieldIDs: {
post: this.props.post.id,
},
}];
}
static fragments = {
post: () => Relay.QL`
fragment on Post {
id,
}
`,
};
}
The client-side mutation is invoked like this:
Relay.Store.commitUpdate(new AddTagToPostMutation({
post: postToModify,
name: tagName,
}));
I think you should just use FIELDS_CHANGE in such situations.
getConfigs() {
return [{
type: 'FIELDS_CHANGE',
fieldIDs: {post: this.props.post.id},
}];
}
getOptimisticResponse() {
return {
post: {
id: this.props.post.id,
tags: [...this.props.post.tags, this.props.tag],
},
};
}