Examples

BrowserSm Examples

This page provides practical examples of using BrowserSm for various tasks, from basic VM usage to complex browser applications.

Table of Contents

  1. Basic VM Usage
  2. Smalltalk Development
  3. Browser Application Examples
  4. Web Page Scripting
  5. Custom Extensions
  6. Performance Examples

Basic VM Usage

Loading and Running Images

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Load a Squeak image in the browser
const vm = new BrowserSm.VM();

// Load from URL
await vm.loadImage('images/squeak6.0-basic.image');

// Load from file input
const fileInput = document.getElementById('imageFile');
fileInput.addEventListener('change', async (event) => {
    const file = event.target.files[0];
    const arrayBuffer = await file.arrayBuffer();
    await vm.loadImage(arrayBuffer);
});

// Start execution
vm.run();

Interacting with Smalltalk from JavaScript

1
2
3
4
5
6
7
8
9
10
11
12
// Execute Smalltalk code from JavaScript
const result = await vm.evaluate('2 + 3');
console.log(result); // 5

// Create Smalltalk objects
const point = await vm.evaluate('Point x: 10 y: 20');
const x = await vm.evaluate('point x', { point });
console.log(x); // 10

// Call Smalltalk methods
const browser = await vm.evaluate('BSBrowser new');
await vm.evaluate('browser open', { browser });

Smalltalk Development

Creating a Simple Application

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
"Define a simple calculator application"
Object subclass: #Calculator
    instanceVariableNames: 'display accumulator operation'
    classVariableNames: ''
    poolDictionaries: ''
    category: 'Examples-Calculator'.

Calculator>>initialize
    super initialize.
    display := '0'.
    accumulator := 0.
    operation := nil.

Calculator>>enterDigit: digit
    display := (display = '0')
        ifTrue: [ digit asString ]
        ifFalse: [ display , digit asString ].

Calculator>>enterOperation: op
    accumulator := display asNumber.
    operation := op.
    display := '0'.

Calculator>>calculate
    | result |
    result := operation
        ifNil: [ accumulator ]
        ifNotNil: [
            accumulator perform: operation with: display asNumber
        ].
    display := result printString.
    accumulator := result.
    operation := nil.

"Usage"
calc := Calculator new.
calc enterDigit: 5.
calc enterOperation: #+.
calc enterDigit: 3.
calc calculate.  "Returns '8'"

Building a Morphic UI

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
"Create a simple window with buttons"
Morph subclass: #SimpleWindow
    instanceVariableNames: 'buttons textArea'
    classVariableNames: ''
    poolDictionaries: ''
    category: 'Examples-UI'.

SimpleWindow>>initialize
    super initialize.
    self extent: 300@200.
    self color: Color lightGray.
    self setupUI.

SimpleWindow>>setupUI
    "Add text area"
    textArea := PluggableTextMorph new.
    textArea position: 10@10.
    textArea extent: 280@120.
    self addMorph: textArea.

    "Add buttons"
    buttons := OrderedCollection new.
    #('Save' 'Load' 'Clear') withIndexDo: [ :label :index |
        | button |
        button := SimpleButtonMorph new.
        button label: label.
        button position: (10 + (index - 1 * 70))@140.
        button extent: 60@30.
        button target: self.
        button actionSelector: ('handle', label) asSymbol.
        buttons add: button.
        self addMorph: button.
    ].

SimpleWindow>>handleSave
    FileDirectory default writeFileNamed: 'data.txt' contents: textArea text.

SimpleWindow>>handleLoad
    | content |
    content := FileDirectory default contentsOfFileNamed: 'data.txt'.
    textArea setText: content.

SimpleWindow>>handleClear
    textArea setText: ''.

"Open the window"
SimpleWindow new openInWorld.

Browser Application Examples

Custom HTML Element

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
"Create a custom progress bar element"
BSDOMElement subclass: #BSProgressBar
    instanceVariableNames: 'value maximum animated'
    classVariableNames: ''
    poolDictionaries: ''
    category: 'Examples-CustomElements'.

BSProgressBar class>>tagName
    ^ 'progress-bar'

BSProgressBar>>initialize
    super initialize.
    self tagName: 'progress-bar'.
    value := 0.
    maximum := 100.
    animated := false.
    self createStructure.

BSProgressBar>>createStructure
    "Create internal DOM structure"
    | container bar |
    container := BSDOMElement tag: 'div'.
    container setAttribute: 'class' value: 'progress-container'.

    bar := BSDOMElement tag: 'div'.
    bar setAttribute: 'class' value: 'progress-bar-fill'.

    container appendChild: bar.
    self appendChild: container.

BSProgressBar>>value: newValue
    value := newValue min: maximum max: 0.
    self updateDisplay.

BSProgressBar>>updateDisplay
    | percentage bar |
    percentage := (value / maximum * 100) rounded.
    bar := self querySelector: '.progress-bar-fill'.
    bar style setProperty: 'width' value: percentage printString , '%'.

"Register the custom element"
BSHTMLParser registerCustomElement: 'progress-bar' class: BSProgressBar.

"Use in HTML"
"<progress-bar id='myProgress' max='100'></progress-bar>"

Browser Plugin Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
"Create a simple bookmark manager plugin"
Object subclass: #BSBookmarkManager
    instanceVariableNames: 'bookmarks browser'
    classVariableNames: ''
    poolDictionaries: ''
    category: 'Examples-Plugins'.

BSBookmarkManager>>initialize
    super initialize.
    bookmarks := OrderedCollection new.
    self loadBookmarks.

BSBookmarkManager>>install: aBrowser
    browser := aBrowser.
    browser registerPlugin: self.
    self addToolbarButton.

BSBookmarkManager>>addToolbarButton
    | button |
    button := BSToolbarButton new.
    button label: 'Bookmarks'.
    button action: [ self openBookmarkPanel ].
    browser toolbar addButton: button.

BSBookmarkManager>>addBookmark: url title: title
    bookmarks add: (Dictionary new
        at: 'url' put: url;
        at: 'title' put: title;
        at: 'timestamp' put: DateAndTime now;
        yourself).
    self saveBookmarks.

BSBookmarkManager>>openBookmarkPanel
    | panel list |
    panel := BSModalPanel new.
    panel title: 'Bookmarks'.

    list := BSListMorph new.
    bookmarks do: [ :bookmark |
        | item |
        item := BSListItem new.
        item label: (bookmark at: 'title').
        item action: [
            browser navigateTo: (bookmark at: 'url').
            panel close.
        ].
        list addItem: item.
    ].

    panel addMorph: list.
    panel open.

Web Page Scripting

DOM Manipulation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<!DOCTYPE html>
<html>
<head>
    <title>Smalltalk DOM Example</title>
</head>
<body>
    <h1 id="title">Hello, World!</h1>
    <button id="changeColor">Change Color</button>
    <div id="content"></div>

    <script type="text/smalltalk">
        "Get DOM elements"
        title := document getElementById: 'title'.
        button := document getElementById: 'changeColor'.
        content := document getElementById: 'content'.

        "Create color array"
        colors := #('red' 'blue' 'green' 'purple' 'orange').
        currentColor := 1.

        "Add event listener"
        button addEventListener: 'click' handler: [
            | color |
            color := colors at: currentColor.
            title style setProperty: 'color' value: color.
            currentColor := currentColor + 1.
            currentColor > colors size ifTrue: [ currentColor := 1 ].

            "Update content"
            content innerHTML: '<p>Color changed to: ' , color , '</p>'.
        ].

        "Initial setup"
        console log: 'Smalltalk script loaded successfully!'.
    </script>
</body>
</html>

Form Handling

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
<!DOCTYPE html>
<html>
<body>
    <form id="contactForm">
        <input type="text" id="name" placeholder="Name" required>
        <input type="email" id="email" placeholder="Email" required>
        <textarea id="message" placeholder="Message" required></textarea>
        <button type="submit">Send</button>
    </form>

    <script type="text/smalltalk">
        "Form validation and submission"
        form := document getElementById: 'contactForm'.

        form addEventListener: 'submit' handler: [ :event |
            | name email message valid |

            "Prevent default submission"
            event preventDefault.

            "Get form values"
            name := (document getElementById: 'name') value trimBoth.
            email := (document getElementById: 'email') value trimBoth.
            message := (document getElementById: 'message') value trimBoth.

            "Validate"
            valid := true.
            name isEmpty ifTrue: [
                self showError: 'Name is required'.
                valid := false.
            ].
            (email includes: $@) ifFalse: [
                self showError: 'Valid email is required'.
                valid := false.
            ].
            message isEmpty ifTrue: [
                self showError: 'Message is required'.
                valid := false.
            ].

            "Submit if valid"
            valid ifTrue: [
                self submitForm: (Dictionary new
                    at: 'name' put: name;
                    at: 'email' put: email;
                    at: 'message' put: message;
                    yourself).
            ].
        ].

        "Helper methods"
        submitForm: data
            fetch url: '/api/contact'
                method: 'POST'
                headers: (Dictionary new at: 'Content-Type' put: 'application/json'; yourself)
                body: (JSON stringify: data)
                then: [ :response |
                    response ok ifTrue: [
                        self showSuccess: 'Message sent successfully!'
                    ] ifFalse: [
                        self showError: 'Failed to send message'.
                    ].
                ]
                catch: [ :error |
                    console error: error.
                    self showError: 'Network error occurred'.
                ].

        showError: message
            | errorDiv |
            errorDiv := document createElement: 'div'.
            errorDiv className: 'error'.
            errorDiv textContent: message.
            form appendChild: errorDiv.

        showSuccess: message
            | successDiv |
            successDiv := document createElement: 'div'.
            successDiv className: 'success'.
            successDiv textContent: message.
            form appendChild: successDiv.
    </script>
</body>
</html>

Canvas Graphics

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
<!DOCTYPE html>
<html>
<body>
    <canvas id="myCanvas" width="400" height="300"></canvas>
    <button id="animate">Start Animation</button>

    <script type="text/smalltalk">
        "Canvas drawing and animation"
        canvas := document getElementById: 'myCanvas'.
        ctx := canvas getContext: '2d'.
        button := document getElementById: 'animate'.

        "Animation state"
        animating := false.
        ballX := 50.
        ballY := 50.
        ballSpeedX := 2.
        ballSpeedY := 3.
        ballRadius := 20.

        "Drawing method"
        drawFrame := [
            "Clear canvas"
            ctx clearRect: 0 y: 0 width: 400 height: 300.

            "Draw ball"
            ctx fillStyle: 'blue'.
            ctx beginPath.
            ctx arc: ballX y: ballY radius: ballRadius startAngle: 0 endAngle: Float pi * 2.
            ctx fill.

            "Update position"
            ballX := ballX + ballSpeedX.
            ballY := ballY + ballSpeedY.

            "Bounce off walls"
            (ballX + ballRadius > 400 or: [ ballX - ballRadius < 0 ]) ifTrue: [
                ballSpeedX := ballSpeedX negated.
            ].
            (ballY + ballRadius > 300 or: [ ballY - ballRadius < 0 ]) ifTrue: [
                ballSpeedY := ballSpeedY negated.
            ].
        ].

        "Animation loop"
        animate := [
            animating ifTrue: [
                drawFrame value.
                requestAnimationFrame: animate.
            ].
        ].

        "Button handler"
        button addEventListener: 'click' handler: [
            animating := animating not.
            button textContent: (animating
                ifTrue: [ 'Stop Animation' ]
                ifFalse: [ 'Start Animation' ]).
            animating ifTrue: [ animate value ].
        ].

        "Initial draw"
        drawFrame value.
    </script>
</body>
</html>

Custom Extensions

CSS Engine Extension

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
"Add support for CSS Grid Layout"
BSCSSLayoutEngine subclass: #BSCSSGridLayout
    instanceVariableNames: 'gridContainer gridItems columnTracks rowTracks'
    classVariableNames: ''
    poolDictionaries: ''
    category: 'Examples-CSSExtensions'.

BSCSSGridLayout>>layout: element inBox: containerBox
    "Implement CSS Grid layout algorithm"
    self setupGridContainer: element.
    self setupGridTracks.
    self positionGridItems.

BSCSSGridLayout>>setupGridContainer: element
    | gridTemplate |
    gridContainer := element.
    gridItems := element children select: [ :child |
        child computedStyle display ~= 'none'
    ].

    "Parse grid-template-columns and grid-template-rows"
    gridTemplate := element computedStyle getProperty: 'grid-template-columns'.
    self parseGridTemplate: gridTemplate for: #columns.

BSCSSGridLayout>>parseGridTemplate: template for: axis
    "Parse CSS grid template syntax like '1fr 2fr 100px'"
    | tracks |
    tracks := template splitOn: ' '.
    axis = #columns
        ifTrue: [ columnTracks := tracks ]
        ifFalse: [ rowTracks := tracks ].

BSCSSGridLayout>>positionGridItems
    gridItems withIndexDo: [ :item :index |
        | column row |
        column := (index - 1) \\ columnTracks size + 1.
        row := (index - 1) // columnTracks size + 1.
        self positionItem: item inColumn: column row: row.
    ].

Browser Security Extension

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
"Content Security Policy implementation"
Object subclass: #BSContentSecurityPolicy
    instanceVariableNames: 'directives violations browser'
    classVariableNames: ''
    poolDictionaries: ''
    category: 'Examples-Security'.

BSContentSecurityPolicy>>initialize
    super initialize.
    directives := Dictionary new.
    violations := OrderedCollection new.

BSContentSecurityPolicy>>parseHeader: headerValue
    "Parse CSP header: 'default-src 'self'; script-src 'unsafe-eval'"
    | parts |
    parts := headerValue splitOn: ';'.
    parts do: [ :part |
        | directive sources |
        part := part trimBoth.
        directive := (part splitOn: ' ') first.
        sources := (part splitOn: ' ') allButFirst.
        directives at: directive put: sources.
    ].

BSContentSecurityPolicy>>checkScriptExecution: scriptElement
    "Validate script execution against CSP"
    | scriptSrc allowedSources |
    scriptSrc := scriptElement getAttribute: 'src'.
    allowedSources := directives at: 'script-src' ifAbsent: [
        directives at: 'default-src' ifAbsent: [ #() ]
    ].

    ^ self isSourceAllowed: scriptSrc in: allowedSources.

BSContentSecurityPolicy>>isSourceAllowed: source in: allowedSources
    allowedSources do: [ :allowed |
        allowed = '''self''' ifTrue: [ ^ self isSameOrigin: source ].
        allowed = '''unsafe-eval''' ifTrue: [ ^ true ].
        (source beginsWith: allowed) ifTrue: [ ^ true ].
    ].
    ^ false.

BSContentSecurityPolicy>>reportViolation: violation
    violations add: violation.
    console warn: 'CSP violation: ', violation description.

Performance Examples

Optimized DOM Updates

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
"Batch DOM updates for better performance"
Object subclass: #BSDOMBatcher
    instanceVariableNames: 'pendingUpdates scheduledUpdate'
    classVariableNames: ''
    poolDictionaries: ''
    category: 'Examples-Performance'.

BSDOMBatcher>>initialize
    super initialize.
    pendingUpdates := OrderedCollection new.
    scheduledUpdate := false.

BSDOMBatcher>>queueUpdate: updateBlock for: element
    pendingUpdates add: (Dictionary new
        at: 'element' put: element;
        at: 'update' put: updateBlock;
        yourself).
    self scheduleFlush.

BSDOMBatcher>>scheduleFlush
    scheduledUpdate ifFalse: [
        scheduledUpdate := true.
        requestAnimationFrame: [ self flush ].
    ].

BSDOMBatcher>>flush
    "Apply all pending updates in a single frame"
    pendingUpdates do: [ :update |
        (update at: 'update') value: (update at: 'element').
    ].
    pendingUpdates removeAll.
    scheduledUpdate := false.

"Usage"
batcher := BSDOMBatcher new.

"Instead of immediate updates:"
element style setProperty: 'color' value: 'red'.
element style setProperty: 'fontSize' value: '16px'.

"Batch them:"
batcher queueUpdate: [ :el |
    el style setProperty: 'color' value: 'red'.
    el style setProperty: 'fontSize' value: '16px'.
] for: element.

Memory-Efficient Large Lists

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
"Virtual scrolling for large datasets"
Morph subclass: #BSVirtualList
    instanceVariableNames: 'items visibleItems itemHeight containerHeight scrollTop'
    classVariableNames: ''
    poolDictionaries: ''
    category: 'Examples-Performance'.

BSVirtualList>>initialize
    super initialize.
    items := OrderedCollection new.
    visibleItems := OrderedCollection new.
    itemHeight := 30.
    containerHeight := 300.
    scrollTop := 0.
    self setupScrolling.

BSVirtualList>>setItems: itemCollection
    items := itemCollection.
    self updateVisibleItems.

BSVirtualList>>updateVisibleItems
    | startIndex endIndex visibleCount |
    visibleCount := containerHeight // itemHeight + 2. "Buffer items"
    startIndex := scrollTop // itemHeight + 1.
    endIndex := (startIndex + visibleCount) min: items size.

    visibleItems removeAll.
    startIndex to: endIndex do: [ :index |
        visibleItems add: (items at: index).
    ].

    self renderVisibleItems.

BSVirtualList>>renderVisibleItems
    self removeAllMorphs.
    visibleItems withIndexDo: [ :item :index |
        | itemMorph y |
        itemMorph := self createItemMorph: item.
        y := (scrollTop // itemHeight + index - 1) * itemHeight.
        itemMorph position: 0@y.
        self addMorph: itemMorph.
    ].

BSVirtualList>>handleScroll: scrollEvent
    scrollTop := scrollEvent scrollTop.
    self updateVisibleItems.

These examples demonstrate the flexibility and power of BrowserSm, showing how the VM enables both traditional Squeak development and innovative web applications using Smalltalk throughout the entire stack.