WebReflection / hyperHTML

A Fast & Light Virtual DOM Alternative
ISC License
3.07k stars 112 forks source link

How to add the selected attribute to an option? #49

Closed albertosantini closed 7 years ago

albertosantini commented 7 years ago

Correctly this doesn't work, getting Uncaught TypeError: updates[(i - 1)] is not a function:

            <select onchange="${events}">${
                state.items.map(item => hyperHTML.wire(item, ":option")`
                    <option value="${item.id}" 
                        ${state.selectedItem.id === item.id && "selected"}>${item.text}</option>
            `)}</select>

Also I successfully used a conditional statement in the literal templates, wrapped in a function, but it is ugly.

Any magic hint?

P.S.: auto label this issue as help wanted. :)

WebReflection commented 7 years ago
<select onchange="${events}">${
  state.items.map(item => hyperHTML.wire(item, ":option")`
    <option
      value="${item.id}"
      selected="${state.selectedItem.id === item.id}"
    > ${item.text} </option>
`)}</select>
WebReflection commented 7 years ago

TL;DR checked, disabled, etc ... all booleans and/or special attributes present in an element prototype can be assigned like any other attribute as shown in this documentation example: https://github.com/WebReflection/hyperHTML/blob/master/GETTING_STARTED.md#how-to-define-hyperhtml-templates

albertosantini commented 7 years ago

Ops... I read that document a few times, I supposed to read that doc one time more. :)

You are very patience. Thanks. You may add that advice in the GitHub issue Template.

As sign of peace, I share the last version of my proof-of-concept, showing wrist interfacing an Introspected object (toggling the visibility of a list, style inline for demo purpose), sharing Introspected object with the services, nested components, and other.

https://plnkr.co/edit/j21P6fMGGwQhaVHNqhaK?p=preview

h.js

"use strict";

// util.js
class Util {
    static query(selector) {
        return document.querySelector(selector) ||
            console.error(selector, "not found");
    }
}

// foo.template.js - like an external html file
class FooTemplate {
    static update(render, state, events) {
        render`
            <select id="selectItem" onchange="${events}">${
                state.items.map(item => hyperHTML.wire(item, ":option")`
                <option value="${item.id}"
                        selected="${state.selectedItem.id === item.id}">
                    ${item.text}
                </option>
            `)}</select>

            <p>Selected: ${state.selectedItem.text}</p>

            <button id="addItem" type="button"
                onclick="${events}">Add minions to the list
            </button>

            <button id="alertSomething" type="button"
                onclick="${events.bind({ text: "dummy text" })}">Alert something
            </button>

            <input id="toggleList" type="checkbox"
                checked="${state.toggleList}">Toggle the list</input>

            <ul style="${state.toggleList
                ? "display: block;" : "display: none;"}"
                >${state.items.map(item => hyperHTML.wire(item, ":li")`
                    <li> ${item.id} / ${item.text} </li>
            `)}</ul>
        `;
    }
}

// foo.service.js
class FooService {
    constructor(items) {
        this.items = items;
    }

    getItems() {
        return this.items;
    }

    refresh() {
        setTimeout(() => { // simulating an async update of the model
            const lastItem = this.items.slice(-1)[0];

            this.items.push({ id: lastItem.id + 1, text: "minions" });
        }, 2000);
    }
}

// foo.controller.js
class FooController {
    constructor(render, template) {
        const events = e => this.handleEvent(e);

        const items = [
            { id: 1, text: "foo" },
            { id: 2, text: "bar" },
            { id: 3, text: "baz" }
        ];

        this.state = Introspected({
            items,
            selectedItem: items[1],
            toggleList: true
        }, state => template.update(render, state, events));

        this.fooService = new FooService(this.state.items);

        wrist.watch(Util.query("#toggleList"), "checked",
            this.onToggleList.bind(this.state));
    }

    refresh() {
        this.fooService.refresh();
    }

    handleEvent(e) {
        const type = e.type;
        const id = e.target.id || console.warn(e.target, "target without id");
        const method = `on${id[0].toUpperCase()}${id.slice(1)}` +
             `${type[0].toUpperCase()}${type.slice(1)}`;

        return method in this ? this[method](e)
            : console.warn(e.type, "not implemented");
    }

    onSelectItemChange(e) {
        this.state.selectedItem = this.state.items.find(
            item => +e.target.value === item.id
        );
    }

    onAddItemClick() {
        this.refresh();
    }

    onAlertSomethingClick(e, text) {
        console.warn("to be implemented");
    }

    onToggleList(prop, prev, curr) {
        this.toggleList = curr;
    }
}

// foo.component.js
class FooComponent {
    constructor() {
        const render = hyperHTML.bind(Util.query("foo"));

        this.fooController = new FooController(render, FooTemplate);
    }

    refresh() {
        this.fooController.refresh();
    }
}

// root.template.js
class RootTemplate {
    static update(render) {
        render`<foo></foo>`;
    }
}

// root.controller.js
class RootController {
    constructor(render, template) {
        template.update(render);
    }
}

// root.component.js
class RootComponent {
    constructor() {
        const render = hyperHTML.bind(Util.query("root"));

        this.rootController = new RootController(render, RootTemplate);
    }
}

// root.module.js
new RootComponent();

// foo.module.js
// the order is important, otherwise the tag "foo" doesn't exist
const fooComponent = new FooComponent();

// whatever.js - maybe using a singleton for the service underlying used
setTimeout(() => { // simulating an async update without user interaction
    fooComponent.refresh();
}, 3000);

h.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Getting Started with HyperHTML</title>
</head>
<body>
    <root></root>

    <script src="https://unpkg.com/hyperhtml"></script>
    <script src="https://unpkg.com/introspected"></script>
    <script src="https://unpkg.com/wrist"></script>

    <script src="h.js"></script>
</body>
</html>
WebReflection commented 7 years ago

two quick hints.

            <button id="alertSomething" type="button"

                Watch out, events is an arrow function.
                One does not simply bind an arrow function
                this does not do what you think it does
                onclick="${events.bind({ text: "dummy text" })}">Alert something

                ... better ...
                onclick="${(e) => events(e, "dummy text")}">Alert something
            </button>

and

            <input id="toggleList" type="checkbox"
                onchange="${(e) => 'you do not need wrist here'}"
                checked="${state.toggleList}">Toggle the list</input>

wrist was born before hyperHTML and it can be used for some part of the page that does not use hyperHTML but it makes little sense to use wrist for elements created via hyperHTML.

On top of that, don't be shy with events, it can be an object of events to simplify some call.

events = {onchange: e => this.onInputChange(e)} and similar

albertosantini commented 7 years ago

Awesome. Reading minds here. :)

I was just filling an issue how to pass a payload to handleEvent.

Thanks for the tips.

albertosantini commented 7 years ago

Just modified the code, removing also wrist, and it works perfectly. Very elegant. And you answered me before I wrote the question (about that events.bind) in a new issue. :)

In the template:

            <button id="alertSomething" type="button"
                onclick="${e => events(e, "ok")}">Alert something
            </button>

            <input id="toggleList" type="checkbox"
                onchange="${e => state.toggleList = e.target.checked}"
                checked="${state.toggleList}">Toggle the list</input>

In the controller:

    const events = (e, payload) => this.handleEvent(e, payload);
...
    handleEvent(e, payload) {
        const type = e.type;
        const id = e.target.id || console.warn(e.target, "target without id");
        const method = `on${id[0].toUpperCase()}${id.slice(1)}` +
             `${type[0].toUpperCase()}${type.slice(1)}`;

        return method in this ? this[method](e, payload)
            : console.warn(e.type, "not implemented");
    }