Trying to extend rich markdown editor - reactjs

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);
}

Related

Gutenberg Block Variation Picker not working

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:

Returning an arrow function to a component

I have the arrow function below and I want to be able to return everything here to a component.
How do I return everything? Is it possible to use an export with this? The code below resides in
a js file.
const myTester = props => {
[
{
title: props.intl.formatMessage({
id: "ren_past_due"
}),
icon: (
<EventBusy
color="#cc2444"
style={style}
className="icon-box-icon-image material-icons"
/>
),
color: "#C60C30",
expiresBefore: today()
},
{
title: props.intl.formatMessage({
id: "ren_zerotothree_months"
}),
icon: (
<Today
color="#f2b826"
style={style}
className="icon-box-icon-image material-icons"
/>
),
color: "#F0AB00",
expiresAfter: today(),
expiresBefore: today().add(3, "months")
}
].map(item => {
if (item.expiresBefore) {
item.expiresBefore = item.expiresBefore.format("Y-MM-DD");
}
if (item.expiresAfter) {
item.expiresAfter = item.expiresAfter.format("Y-MM-DD");
}
return item;
});
};
Add a return in front of [ in line #2.
Shorthand to return arrays () => ([1, 2, 3]);.
Shorthand to return object () => ({ name: 'shorthand' });.
I'm not sure where you are using this 'javascript' file.
If wanted to use like like a NPM module then just simply do following:
module.exports = myTester;
When importing this file you will get this function
const test = require('./myTester.js');
// test is now test(props);
On front-end on older ways before rectJs and import this would had just worked by calling function myTester(props) and importing file via script.
<script type="text/javascript" src="/myTester.js"></script>
With react or new ways now it's
import myTester from 'myTester'

Passing data from service to angular components

I am reading data from a firebase database and creating some objects based on the data received and pushing the data into a list. But the control goes back to the component before the objects are created or pushed into the list. I am confused to use any life cycle hooks in this approach.
Class Service(){
questions: QuestionsData<any>[]=[];
getQuestions(FormKey: string) {
var dbQuestions = this.af.list('/elements', {
query: {
limitToLast: 200,
orderByChild: 'formid',
equalTo: FormKey
}
})
dbQuestions.subscribe(snapshots=>{
snapshots.forEach(elementData => {
this.questions.push(new TextboxQuestion({
key: elementData.elementname,
label: elementData.displaytext,
value: elementData.elementvalue,
required: false,
order: elementData.sortorder
}))
}
}
}
Can anyone suggest how to consume this data in my component.
As JB Nizet mentioned in the comments, you should not subscribe to the observable and unwrap it in your template as you are currently doing. Angular provides the async pipe to handle that subscription for you. You simply want to map your data to TextBoxQuestion's. You can do that with the following code.
class MyComponent {
questions$: QuestionsData<any>[]=[];
getQuestions(FormKey: string) {
const dbQuestions$ = this.af.list('/elements', {
query: {
limitToLast: 200,
orderByChild: 'formid',
equalTo: FormKey
}
});
this.questions$ = dbQuestions$.map(snapshots =>
snapshots.map(data => new TextBoxQuestion({
key: data.elementname,
// and so on...
});
}
}
If you want to run that when your component initializes, use the OnInit lifecycle hook:
ngOnInit() {
this.getQuestions(/* form key */);
}
And then use the async pipe in your template like this:
<ul>
<li *ngFor="let question of questions$ | async">
{{ question.key }}
</li>
</ul>
Your service should be more or less like this:
import 'rxjs/add/operator/map'
Class Service() {
getQuestions(FormKey: string): Observable<QuestionsData<any>[]> {
return dbQuestions = this.af.list('/elements', {
query: {
limitToLast: 200,
orderByChild: 'formid',
equalTo: FormKey
}
}).map(snapshots=>{
conts questions: QuestionsData<any>[]=[];
snapshots.forEach(elementData => {
questions.push(new TextboxQuestion({
key: elementData.elementname,
label: elementData.displaytext,
value: elementData.elementvalue,
required: false,
order: elementData.sortorder
}))
})
return questions;
})
}
}
And in the component:
serviceInstance.getQuestions(FormKey).subscribe(questions => {
// your code here
})

Meteor + React: Meteor.subscribe('Collection', limit) Triggers Rerender Every List Element

I have a Meteor subscription with some settings, so that I do not publish my whole collection server side. The subscription will be fetched within a createContainer() from meteor/react-meteor-data and displayed in a simple <ul> list, where I also added the document.id to the <li> element as a key.
Unfortunately as soon as I increase the settings.limit in the subscription 2nd subscription argument (Meteor.subscripte('Collections', settings.limit) the whole <ul> list rerenders? What can I do to increase the publication limit, while only adding the new list elements?
P.S. When I publish the total Collection and change the limit in my client via Collection.find({}, {limit: newLimit}).fetch(), react is working as expected: leaving the old elements as they are and just adding the new ones!
Client Side
import React, { Component } from 'react';
import { Locations } from '/both/collections/locations.js';
import { Events } from '/both/collections/events.js';
import { createContainer } from 'meteor/react-meteor-data';
class Content extends React.Component {
constructor(props) {
super(props);
this.renderLocations = this.renderLocations.bind(this);
}
renderLocations() {
return this.props.locations.map(function(location) {
return (<li key={location._id} >{location.name}</li>);
});
}
render() {
console.log(this.props);
return !this.props.loading && (
<div>
<ul>
{this.renderLocations()}
</ul>
<h1 onClick={this.props.increaseLimit}> Limit </h1>
<div style={{marginBottom: "100px"}}></div>
</div>
);
}
}
export default createContainer((props) => {
const settings = {
limit: props.limit,
}
const locationsSubscribe = Meteor.subscribe('Locations', settings);
const loading = !locationsSubscribe.ready();
if(loading) {
return { loading };
} else {
const _locations = Locations.find({}, {fields: { name: 1}, sort: { name: 1 }}).fetch();
return {
loading,
locations: _locations,
increaseLimit: props.increaseLimit,
};
}
}, Content);
Server Side
Meteor.publish('Locations', function(settings) {
return Locations.find({}, {limit: settings.limit, sort: { name: 1} } );
});
The Collection.find().fetch() response
[
{
"name": "3-Master Bike-Style",
"_id": "N9rWyZMdxEe6jhNW2"
},
{
"name": "43einhalb",
"_id": "bPgpBm59LohGLaAsf"
},
{
"name": "A&B Döner",
"_id": "qTNMk73ThvaPxGWqM"
},
{
"name": "A.T.U ",
"_id": "aWzSmp2zZ8etDhHk6"
},
{
"name": "AIKO Sushibar - Hot Wok",
"_id": "9pQJgeBNo5gFRkKdF"
},
{
"name": "AXA Stefan Hahn",
"_id": "d9f6mTrSTGCoeKPbP"
}
]
The problem is in your server side logic.
Your current code:
Meteor.publish('Locations', function(settings) {
return Locations.find({}, {limit: settings.limit, sort: { name: 1} } );
});
This will send n number of doc, basically you are 10, 20, 30 and so on docs to the client.
Fix : You need to skip the previous doc.
Solution:
Meteor.publish('Locations', function(settings) {
return Locations.find({}, {skip: settings.skip, limit: settings.limit, sort: { name: 1} } );
});
Or
Meteor.publish('Locations', function(settings) {
var skip = settings.pageNumber * settings.number_of_record_per_page; //change variable according to you
return Locations.find({}, {skip: settings.limit, limit: settings.limit, sort: { name: 1} } );
});
Ok I finally found the Problem:
The every time the limit changes the createContainer() subscribes again to the published collection! Meaning that it sends new props to my Content component, which triggers a rerender! For the short moment it takes to resubscribe the location array, which is saved as a prop, will be overridden by an empty array, which one only notes as a flash in the screen. So for a short second there will be no content displayed and then the new props with the correct locations array is transmitted.
The solutions now to concat() the new location array into the state of the Content component via componentWillReceiveProps(nextProps) whenever there are new locations to add. Then one can compare the old and new state, within the shouldComponentUpdate(nextProps, nextState) and only update when the state is changing!

My query is failing in relay and I don't know why?

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.

Resources