Missing Dom nodes

I’m working on a block editor for fairly straightfoward email templates. The templates have a restricted set of elements. As a starting point I’m creating a custom schema. The initial state of a document should be a template node with a child row node and the row should have a column node. Fow now I’m allowing the column node to contain block+, and the only node in the block group is the paragraph. It appears that the nodes are all created as expected:

Inspecting the dom however shows only the template div rendered out as a tag. A cursor does appear in the editor but when typing no character is displayed, and the dom is not updated.

I need the nodes to all be rendered out to the DOM so that the user can interact with them. Additionally is there a way to dump the node structure that is less painful that using the debug tools inspector?

Any help or insight would be appreciated.

==schema.ts==

import { AstTransformer, NotExpr } from "@angular/compiler";
import {Schema, NodeSpec, MarkSpec, DOMOutputSpec} from "prosemirror-model"

const pDOM: DOMOutputSpec = ["p", 0], blockquoteDOM: DOMOutputSpec = ["blockquote", 0],
      hrDOM: DOMOutputSpec = ["hr"], brDOM: DOMOutputSpec = ["br"];

export type textAlignValues="center"|"left"|"right"|"justify";

const templateNode:NodeSpec={
    
    attrs:{
        style:{default:"width:100%;min-height:200px;display:flex;justify-content:center;background-color:#FAFAFA;"},
        class:{default:"wcTemplate"}
    },
    content:"row+",//At least one row must be present
    group:"template",
    toDOM:node=>{
        const attrs = {
            style:node.attrs.style,
            class:node.attrs.class
        };
        return ['div',attrs]
    },
    parseDOM:[
        {
            tag:'div.wcTemplate',
            getAttrs:(dom:HTMLElement|string)=>{
                if(typeof(dom) === "string"){
                    return false;                
                };
                return {
                    style:dom.getAttribute('style'),
                    class:dom.getAttribute('class')
                };
            }
            
        }        
    ]
};

const rowNode:NodeSpec={
    attrs:{
        rowtype:{default:0},
        style:{default:"display:flex;justify-content:center;width:500px;padding:5px;background-color:red;flex-direction:row;flex-wrap:wrap;"},
        class:{default:"wcLayoutRow"}
    },
    content:"column+",
    group:"row",
    toDom:node=>{
        const attrs = {
            style:node.attrs.style,
            class:node.attrs.class
        };
        return ['div',attrs];
    },
    parseDOM:[
        {
            tag:"div.wcLayoutRow",
            getAttrs:(dom:HTMLElement|string)=>{
                if(typeof(dom)==="string"){
                    return false;
                }
                return {
                    style:dom.getAttribute("style"),
                    class:dom.getAttribute("class")
                }
            }
        }
    ]

};

const colNode:NodeSpec={
    attrs:{
        colwidth:{default:1},
        style:{default:"display:flex;justify-content:center;width:500px;padding:5px;background-color:blue;flex-direction:column;flex:1;"},
        class:{default:"wcLayoutColumn"}
    },
    content:"block+",
    group:"column",
    toDom:node=>{
        const attrs = {
            style:node.attrs.style,
            class:node.attrs.class
        };
        return ['div',attrs];
    },
    parseDOM:[
        {
            tag:"div.wcLayoutColumn",
            getAttrs:(dom:HTMLElement|string)=>{
                if(typeof(dom)==="string"){
                    return false;
                }
                return {
                    style:dom.getAttribute("style"),
                    class:dom.getAttribute("class")
                }
            }
        }
    ]
}


 /// A plain paragraph textblock. Represented in the DOM
  /// as a `<p>` element.
  const paragraph:NodeSpec= {
    content: "inline*",
    group: "block",
    parseDOM: [{tag: "p"}],
    toDOM() { return pDOM }
  };

    /// The text node.
const text:NodeSpec ={
        group: "inline"
      };
    
const docNode:NodeSpec={
    content:"template"
}
const nodes = {
    doc:docNode,
    templateNode,
    rowNode,
    colNode,
    paragraph,
    text
};

const marks = {};//schema.spec.marks;
export const wcSchema = new Schema({
    nodes,
    marks
});

===emaileditor.component.ts=======

import { AfterViewInit, Component, ViewChild,OnInit, Input } from '@angular/core';
import { EditorState,Transaction } from 'prosemirror-state';
import { EditorView } from 'prosemirror-view';
import { Schema, DOMParser,DOMSerializer } from 'prosemirror-model';
import { schema } from 'prosemirror-schema-basic';
import { addListNodes } from 'prosemirror-schema-list';
import { exampleSetup } from 'prosemirror-example-setup';
import {wcSchema} from './schema';



@Component({
  selector: 'app-emaileditor',
  templateUrl: './emaileditor.component.html',
  styleUrls: ['./emaileditor.component.css']
})
export class EmaileditorComponent implements OnInit,AfterViewInit {
 
  @ViewChild('editor') editor;
  @ViewChild('content') content;
  @ViewChild('display') display:HTMLParagraphElement|undefined;

  public docvalue:string|null="<p>test</p>";
  constructor() { }
  
  
  private view:EditorView|undefined;

  ngOnInit(): void {
  }

  ngAfterViewInit(): void {
        // Mix the nodes from prosemirror-schema-list into the basic schema to
    // create a schema with list support.
    
    this.view = new EditorView(this.editor.nativeElement, {
      state: EditorState.create({doc: DOMParser.fromSchema(wcSchema).parse(this.content.nativeElement), plugins: exampleSetup({ schema: wcSchema })}),
      dispatchTransaction:(tr:Transaction)=>{
        console.log("Document size went from ", tr.before.content.size," to ",tr.doc.content.size);
        if(this.view){
          let newState = this.view?.state.apply(tr);
          this.view.updateState(newState);
  
        }
        this.docvalue=this.toHTML()??'';        
      }
    });
    //const row=wcSchema.nodes.rowNode.create();
    //const tr = this.view.state.tr.insert(this.view.state.selection.from,  wcSchema.node("rowNode"));
    //const rowstate = this.view.state.apply(tr);
    //this.view.updateState(rowstate);
  }


  //Utilies
  public toHTML():string|null{
    let ret:string|null=null;
    if(this.view)
    {
      var node = this.view.state.doc;//.nodeAt(0);
      if(node){
        const fragment = DOMSerializer.fromSchema(wcSchema).serializeFragment(node.content);
        const div = document.createElement('div');
        div.appendChild(fragment);
        ret=div.innerHTML;
      }    
    }
    return ret;
}

}

I presume because your toDOM is not including the zero/hole to indicate where the content is included. e.g. return ['div’,attrs, 0] Though I didn’t try it to see if there are any other issues.

2 Likes

That was it.

For some reason after reading the docs I had the ‘content-hole’ logic completely backwards.

Thanks for the quick reply.