Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
260 changes: 260 additions & 0 deletions examples/custom-elements.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
import {renderer} from "@b9g/crank/dom";
import {createCustomElementClass} from "@b9g/crank/custom-elements";

// Revive the classic <marquee> element!
function* Marquee({speed = 50, children, ref}) {
let position = 0;
let containerWidth = 0;
let contentWidth = 0;

ref?.((element) => ({
start() {
this.play();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The object is going to be assigned to the element why not use element.play(). The this of the Crank component are separate but linked, and the context does not have this.play(), this.pause() on it.

},
stop() {
this.pause();
},
play() {
element.setAttribute("playing", "true");
},
pause() {
element.removeAttribute("playing");
},
onmarqueebounce: null,
}));

this.after((el) => {
const container = el.querySelector(".marquee-container");
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we do host styles.

const content = el.querySelector(".marquee-content");
containerWidth = container.offsetWidth;
contentWidth = content.offsetWidth;
});

const interval = setInterval(() => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let’s use raf, and let’s use the refresh() callback pattern introduced in 0.7

if (this.getAttribute("playing") === "true") {
this.refresh();
}
}, 16); // ~60fps

try {
for ({speed, children} of this) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the type of speed just string?

position += speed / 60; // Adjust for ~60fps

// Bounce effect
if (position > containerWidth) {
position = -contentWidth;
this.dispatchEvent(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The events should likely be dispatched in an after to prevent re-entrancy issues (rendering while rendering)

new CustomEvent("marqueebounce", {
detail: {position},
bubbles: true,
}),
);
}

yield (
<div
class="marquee-container"
style={{
position: "relative",
overflow: "hidden",
width: "100%",
height: "30px",
}}
>
<div
class="marquee-content"
style={{
position: "absolute",
left: `${position}px`,
whiteSpace: "nowrap",
}}
>
{children}
</div>
</div>
);
}
} finally {
clearInterval(interval);
}
}

// Revive the classic <blink> element!
function* Blink({rate = 1000, children, ref}) {
let visible = true;

ref?.((element) => ({
show() {
element.setAttribute("visible", "true");
},
hide() {
element.setAttribute("visible", "false");
},
toggle() {
const current = element.getAttribute("visible");
element.setAttribute("visible", current === "false" ? "true" : "false");
},
onblink: null,
}));

const interval = setInterval(() => {
visible = !visible;
this.setAttribute("visible", visible.toString());
this.refresh();

// Fire blink event
this.dispatchEvent(
new CustomEvent("blink", {
detail: {visible},
bubbles: true,
}),
);
}, rate);

try {
for ({rate, children} of this) {
const isVisible = this.getAttribute("visible") !== "false";

yield (
<span
style={{
visibility: isVisible ? "visible" : "hidden",
}}
>
{children}
</span>
);
}
} finally {
clearInterval(interval);
}
}

// Create custom element classes
const MarqueeElement = createCustomElementClass(Marquee, {
observedAttributes: ["speed", "playing"],
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You know this could be generated based on the types of the parameters, but doing it explicity is nice I guess.

});

const BlinkElement = createCustomElementClass(Blink, {
observedAttributes: ["rate", "visible"],
});

// Register custom elements
customElements.define("x-marquee", MarqueeElement);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rip-marquee, rip-blink. Put blink first because it’s an easier component to write.

customElements.define("x-blink", BlinkElement);

// Demo app
const style = `
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.demo-section {
margin: 30px 0;
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
}
.controls {
margin-top: 10px;
}
button {
margin-right: 10px;
padding: 5px 15px;
cursor: pointer;
}
x-marquee {
display: block;
background: #f0f0f0;
padding: 5px;
margin: 10px 0;
}
x-blink {
font-weight: bold;
color: red;
}
`;

document.addEventListener("DOMContentLoaded", () => {
renderer.render(
<div>
<style>{style}</style>
<h1>Retro Web Elements with Crank Custom Elements!</h1>

<div class="demo-section">
<h2>The Glorious Marquee</h2>
<x-marquee speed="30" playing="true">
🎉 Breaking News: The 90s are back! Marquee elements are cool again!
🎉
</x-marquee>

<x-marquee speed="60">
<span style="color: blue">This marquee is faster! </span>
<span style="color: red">And more colorful! </span>
<span style="color: green">Click play to start! </span>
</x-marquee>

<div class="controls">
<button onclick="document.querySelectorAll('x-marquee').forEach(m => m.play())">
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Crank does not allow arbitrary code as string in event callbacks. You might have to write a component which consumes to marquee and has state with a generator.

Play All
</button>
<button onclick="document.querySelectorAll('x-marquee').forEach(m => m.pause())">
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here.

Pause All
</button>
</div>
</div>

<div class="demo-section">
<h2>The Infamous Blink</h2>
<p>
Remember when <x-blink>EVERYTHING BLINKED</x-blink> on the web? Now
you can make <x-blink rate="500">anything blink</x-blink> again!
</p>

<p>
<x-blink rate="2000" visible="true">
This blinks slowly and thoughtfully
</x-blink>
</p>

<div class="controls">
<button onclick="document.querySelectorAll('x-blink').forEach(b => b.toggle())">
Toggle All Blinks
</button>
<button onclick="document.querySelectorAll('x-blink').forEach(b => b.show())">
Show All
</button>
<button onclick="document.querySelectorAll('x-blink').forEach(b => b.hide())">
Hide All
</button>
</div>
</div>

<div class="demo-section">
<h2>Event Playground</h2>
<p>Open the console to see custom events!</p>
<x-marquee id="event-marquee" speed="80" playing="true">
Watch for bounce events! →
</x-marquee>
<x-blink id="event-blink">Blink events fire on each toggle!</x-blink>
</div>
</div>,
document.body,
);

// Set up event listeners
const eventMarquee = document.getElementById("event-marquee");
const eventBlink = document.getElementById("event-blink");

eventMarquee.onmarqueebounce = (e) => {
// eslint-disable-next-line no-console
console.log("Marquee bounced!", e.detail);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Examples should avoid having console.log calls. Rather you should add to the example in some way that is visible.

};

eventBlink.onblink = (e) => {
// eslint-disable-next-line no-console
console.log("Blink toggled! Visible:", e.detail.visible);
};
});
8 changes: 8 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,14 @@
"import": "./dist/crank.js",
"require": "./dist/crank.cjs"
},
"./custom-elements": {
"import": "./dist/custom-elements.js",
"require": "./dist/custom-elements.cjs"
},
"./custom-elements.js": {
"import": "./dist/custom-elements.js",
"require": "./dist/custom-elements.cjs"
},
"./dom": {
"import": "./dist/dom.js",
"require": "./dist/dom.cjs"
Expand Down
1 change: 1 addition & 0 deletions rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ function copyPackage() {
const input = [
"src/async.ts",
"src/crank.ts",
"src/custom-elements.ts",
"src/dom.ts",
"src/event-target.ts",
"src/jsx-runtime.ts",
Expand Down
Loading