Getting element content into a ViewModel variable


#1

I’m posting this here because it took me a while to figure out how to do this and hopefully I’ll learn a better way of doing it.

I wanted to get a custom elements data that would get inserted into a <slot></slot> into a variable that I could play around with.

For example, we have a pattern library and we want to show code samples. I want a custom element where I can put sample code in side the custom element and have the component render the code, not the html.

In my first attempt I was able to get something working by modifying the content with the @processContent decorator.

But by getting the content into a variable that I could work with in my view model, I can do more with it, such toggle between rendering the code or rendering the html.

toggle-code.html:

<template>
    <span style="display: none"><slot></slot></span>
    
    <style>
      pre {
        background-color: #eee;
        border: 1px solid #999;
        padding: 3px 9px;
      }
    </style>
    
    <div style="${visible ? '' : 'display:none'}">
        <div style="${render ? 'display:none' : ''}">
            <pre><code class="language-markup" au-syntax textcontent.bind="code"></code></pre>
        </div>
        <div style="${render ? '' : 'display:none'}" innerhtml.bind="code"></div>
        <button click.delegate="renderCode()">Render</button>
    </div>
    <button click.delegate="toggleCodeBlock()">${visible ? 'Hide' : 'Show'} Code</button>
    <br><br>
</template>

toggle-code.js

import {processContent, child} from 'aurelia-framework';


@processContent((compiler, resources, node) => {
    const originalContent = node.innerHTML;
    
    node.innerHTML = `<hackity-hack style="display:none" data.bind='${JSON.stringify(originalContent)}'></hackity-hack>`;    
    
    return true;
})
export class toggleCode {
    @child('hackity-hack') hackData;
  
    attached() {
        this.visible = true;
        this.render = false;
        this.code = this.hackData.data.trim();
    }

    toggleCodeBlock() {
        this.visible = !this.visible;
    }
    
    renderCode() {
        this.render = !this.render;
    }
}

See this gist for example: https://gist.run/?id=207296739baf5ad6948fbfc6167f1ad7

I would love to get some extra eyeballs on this to see if this is the best way to go about it, or if there is an easier way to do it.


#2

Why not inject the element into your view-model class, and then grab its inner HTML?

import {inject} from 'aurelia-framework';

@inject(Element)
export class toggleCode {
    constructor(element) {
        this.innerHTML = element.innerHTML;
    }
}

And if you want to be sure Aurelia does not process the code inside the toggle-code element, just add a @processContent(false) decorator on the view-model class.


#3

This is an interesting idea. I noticed that it wrapped the content with <au-content> I would just have to pull that out. Also, it seem to “fix” html for you. Like if I do a simple table with no <tbody> tag, it will throw the <tbody> tag in there for me.


#4

If you add @processContent(false), Aurelia does nothing with the HTML, so <au-content> won’t be added: GistRun


#5

I tried that earlier but then it rendered the html when I didn’t want it to.


#6

Here’s another way of doing it. Pretty annoying having to replace the slot comment node.
https://gist.run/?id=36953664e4f241c5ce8e0c92d286d2af

<template>
    
    <style>
      pre {
        background-color: #eee;
        border: 1px solid #999;
        padding: 3px 9px;
      }
    </style>
    
    <div show.bind="visible">
        <div show.bind="!render">
            <pre><code class="language-markup" au-syntax textcontent.bind="code"></code></pre>
        </div>
        <div ref="slotContainer" show.bind="render"><slot></slot></div>
        <button click.delegate="renderCode()">Render</button>
    </div>
    <button click.delegate="toggleCodeBlock()">${visible ? 'Hide' : 'Show'} Code</button>
    <br><br>
</template>
export class toggleCode {
  
    attached() {
        this.visible = true;
        this.render = false;
        this.code = this.slotContainer.innerHTML.replace('<!--slot-->', '');
    }

    toggleCodeBlock() {
        this.visible = !this.visible;
    }
    
    renderCode() {
        this.render = !this.render;
    }
}

#7

Oh nice. This does seem a lot more simple. slotContainer - going to have to remember this one!


#8

I think it is the “ref” attribute that is key. You can reference any element using ref.


#9

Well, I thought this would work, but the problem is my code sample has aurelia components. they get rendered into their html with this method. So the code samples don’t work.


#10

I’ve come up with a solution that works for aurelia components and doesn’t modify the html:

my-code-sample.html

    <template>
        <span style="display: none"><slot></slot></span>
        
        <style>
          pre {
            background-color: #eee;
            border: 1px solid #999;
            padding: 3px 9px;
          }
        </style>
        
        <div>
            <div>
                <pre><code class="language-markup" au-syntax textcontent.bind="code"></code></pre>
            </div>
            <template replaceable part="rendered"></template>
        </div>

        <br><br>
    </template>

my-code-sample.js

    import {processContent, child} from 'aurelia-framework';

    @processContent((compiler, resources, node) => {
        const originalContent = node.innerHTML;
        const stringified = JSON.stringify(originalContent);
        
        node.innerHTML = `<code-sample data.bind='${stringified}'></code-sample>`; 
        node.innerHTML += `<template replace-part="rendered">${originalContent}</template>`;
        
        return true;
    })
    export class MyCodeSample {
        @child('code-sample') codeSample;
      
        attached() {
            this.code = this.codeSample.data;
        }
    }